Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ smithery mcp publish <bundle.mcpb> -n <org/server> # Publish an MCP bundle
```bash
# Search and connect to an MCP server
smithery mcp search "github"
smithery mcp add https://server.smithery.ai/github --id github
smithery mcp add github --id github

# Find and call tools from your connected MCP servers
smithery tool find "create issue"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@anthropic-ai/mcpb": "^1.1.1",
"@biomejs/biome": "2.3.10",
"@modelcontextprotocol/sdk": "^1.25.3",
"@smithery/api": "^0.64.2",
"@smithery/api": "^0.66.0",
"@smithery/sdk": "^4.1.0",
"@types/inquirer": "^8.2.4",
"@types/inquirer-autocomplete-prompt": "^3.0.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 32 additions & 1 deletion src/commands/__tests__/mcp-add-impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ vi.mock("../mcp/api", () => ({
ConnectSession: {
create: mockCreateSession,
},
connectionTargetFromInput: (input: string) =>
input.startsWith("http://") || input.startsWith("https://")
? { mcpUrl: input }
: { server: input },
}))

vi.mock("../mcp/output-connection", () => ({
Expand Down Expand Up @@ -97,7 +101,10 @@ describe("mcp add duplicate handling", () => {
],
})

await addServer("calclavia/test-input-required-two", {})
await addServer(
"https://server.smithery.ai/calclavia/test-input-required-two",
{},
)

expect(mockCreateConnection).not.toHaveBeenCalled()
expect(mockOutputConnectionDetail).toHaveBeenCalledWith(
Expand All @@ -112,6 +119,30 @@ describe("mcp add duplicate handling", () => {
)
})

test("creates registry connections with the server target", async () => {
mockCreateConnection.mockResolvedValue({
connectionId: "github",
name: "github",
mcpUrl: "https://server.smithery.ai/github",
metadata: null,
status: {
state: "connected",
},
})

await addServer("github", {})

expect(mockListConnectionsByUrl).not.toHaveBeenCalled()
expect(mockCreateConnection).toHaveBeenCalledWith(
{ server: "github" },
{
name: undefined,
metadata: undefined,
headers: undefined,
},
)
})

test("prints setupUrl for auth_required duplicate connections", async () => {
mockListConnectionsByUrl.mockResolvedValue({
connections: [
Expand Down
4 changes: 4 additions & 0 deletions src/commands/__tests__/mcp-add-uplink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ vi.mock("../mcp/api", () => ({
ConnectSession: {
create: mockCreateSession,
},
connectionTargetFromInput: (input: string) =>
input.startsWith("http://") || input.startsWith("https://")
? { mcpUrl: input }
: { server: input },
}))

vi.mock("../../lib/uplink", async () => {
Expand Down
41 changes: 41 additions & 0 deletions src/commands/__tests__/mcp-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ describe("ConnectSession uplink compatibility", () => {
})
})

test("creates registry connections with server targets", async () => {
const create = vi.fn().mockResolvedValue({
connectionId: "exa",
name: "exa",
mcpUrl: "https://server.smithery.ai/exa",
metadata: null,
status: { state: "connected" },
})

const session = new ConnectSession(
{ connections: { create } } as never,
"calclavia",
)
await session.createConnection({ server: "exa" })

expect(create).toHaveBeenCalledWith("calclavia", {
server: "exa",
})
})

test("does not replace conflicting uplink connections on 409", async () => {
const conflict = new ConflictError(409, {}, undefined, new Headers())
const set = vi.fn().mockRejectedValueOnce(conflict)
Expand Down Expand Up @@ -80,6 +100,27 @@ describe("ConnectSession uplink compatibility", () => {
})
})

test("sets registry connections with server targets", async () => {
const set = vi.fn().mockResolvedValue({
connectionId: "exa",
name: "exa",
mcpUrl: "https://server.smithery.ai/exa",
metadata: null,
status: { state: "connected" },
})

const session = new ConnectSession(
{ connections: { set } } as never,
"calclavia",
)
await session.setConnection("exa", { server: "exa" })

expect(set).toHaveBeenCalledWith("exa", {
namespace: "calclavia",
server: "exa",
})
})

test("lists tools over smithery.run REST", async () => {
const get = vi.fn().mockResolvedValue({
tools: [
Expand Down
17 changes: 9 additions & 8 deletions src/commands/mcp/add-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import {
completeConnectionAuthorization,
finalizeAddedConnection,
} from "./add-flow"
import { ConnectSession } from "./api"
import { ConnectSession, connectionTargetFromInput } from "./api"
import { isInputRequiredStatus } from "./connection-status"
import { normalizeMcpUrl } from "./normalize-url"
import { outputConnectionDetail } from "./output-connection"
import { parseJsonObject } from "./parse-json"

Expand All @@ -29,13 +28,15 @@ export async function addServer(
true,
)

const normalizedUrl = normalizeMcpUrl(mcpUrl)
const target = connectionTargetFromInput(mcpUrl)
const session = await ConnectSession.create(options.namespace)

// Check for existing connections with the same URL
if (!options.force) {
const { connections: existing } =
await session.listConnectionsByUrl(normalizedUrl)
// URL inputs can still be checked exactly. Registry-name inputs are sent
// as `server` so Connect owns canonical URL resolution.
if (!options.force && target.mcpUrl) {
const { connections: existing } = await session.listConnectionsByUrl(
target.mcpUrl,
)
if (existing.length > 0) {
let match = existing[0]
const status = match.status?.state ?? "unknown"
Expand Down Expand Up @@ -76,7 +77,7 @@ export async function addServer(
}
}

const connection = await session.createConnection(normalizedUrl, {
const connection = await session.createConnection(target, {
name: options.name,
metadata: parsedMetadata,
headers: parsedHeaders,
Expand Down
5 changes: 2 additions & 3 deletions src/commands/mcp/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
addBundleUplinkServer,
type BundleAddTarget,
} from "./add-uplink-bundle"
import { ConnectSession } from "./api"
import { normalizeMcpUrl } from "./normalize-url"
import { ConnectSession, connectionTargetFromInput } from "./api"
import { outputConnectionDetail } from "./output-connection"
import { parseJsonObject } from "./parse-json"
import { classifyAddTarget } from "./uplink-target"
Expand Down Expand Up @@ -69,7 +68,7 @@ export async function addServer(
const session = await ConnectSession.create(options.namespace)
const connection = await session.setConnection(
options.id,
normalizeMcpUrl(mcpUrl),
connectionTargetFromInput(mcpUrl),
{
name,
metadata: parsedMetadata,
Expand Down
46 changes: 32 additions & 14 deletions src/commands/mcp/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Connection,
ConnectionCreateParams,
ConnectionListParams,
ConnectionSetParams,
ConnectionsListResponse,
} from "@smithery/api/resources/connections.js"
import { createSmitheryClient } from "../../lib/smithery-client"
Expand All @@ -14,7 +15,9 @@ import {
} from "../../utils/smithery-settings"

export type { Connection, ConnectionsListResponse }
export type ConnectionTransport = NonNullable<Connection["transport"]>
export type ConnectionTarget =
| (Required<Pick<ConnectionCreateParams, "mcpUrl">> & { server?: never })
| (Required<Pick<ConnectionCreateParams, "server">> & { mcpUrl?: never })

export interface Trigger {
name: string
Expand Down Expand Up @@ -156,12 +159,12 @@ export class ConnectSession {
}

async createConnection(
mcpUrl?: string,
target?: string | ConnectionTarget,
options: ConnectionWriteOptions = {},
): Promise<Connection> {
return this.smitheryClient.connections.create(
this.namespace,
buildConnectionBody(mcpUrl, options) as ConnectionCreateParams,
buildConnectionBody(target, options),
)
}

Expand All @@ -171,20 +174,20 @@ export class ConnectSession {
*/
async setConnection(
connectionId: string,
mcpUrl?: string,
target?: string | ConnectionTarget,
options: ConnectionWriteOptions = {},
): Promise<Connection> {
try {
return await this.smitheryClient.connections.set(
connectionId,
buildConnectionSetParams(this.namespace, mcpUrl, options),
buildConnectionSetParams(this.namespace, target, options),
)
} catch (error) {
if (error instanceof ConflictError && options.transport !== "uplink") {
await this.deleteConnection(connectionId)
return this.smitheryClient.connections.set(
connectionId,
buildConnectionSetParams(this.namespace, mcpUrl, options),
buildConnectionSetParams(this.namespace, target, options),
)
}
throw error
Expand Down Expand Up @@ -255,31 +258,46 @@ export class ConnectSession {
}
}

export function connectionTargetFromInput(input: string): ConnectionTarget {
return isHttpUrl(input) ? { mcpUrl: input } : { server: input }
}

function buildConnectionBody(
mcpUrl: string | undefined,
target: string | ConnectionTarget | undefined,
options: ConnectionWriteOptions,
): Record<string, unknown> {
const body = {
...(mcpUrl ? { mcpUrl } : {}),
): ConnectionCreateParams {
return {
...normalizeConnectionTarget(target),
...(options.name ? { name: options.name } : {}),
...(options.metadata ? { metadata: options.metadata } : {}),
...(options.headers ? { headers: options.headers } : {}),
...(options.transport ? { transport: options.transport } : {}),
}
return body
}

function buildConnectionSetParams(
namespace: string,
mcpUrl: string | undefined,
target: string | ConnectionTarget | undefined,
options: ConnectionWriteOptions,
) {
): ConnectionSetParams {
return {
namespace,
...buildConnectionBody(mcpUrl, options),
...buildConnectionBody(target, options),
}
}

function normalizeConnectionTarget(
target: string | ConnectionTarget | undefined,
): Pick<ConnectionCreateParams, "mcpUrl" | "server"> {
if (!target) return {}
if (typeof target === "string") return { mcpUrl: target }
return target
}

function isHttpUrl(value: string): boolean {
return value.startsWith("http://") || value.startsWith("https://")
}

function namespacePath(namespace: string): string {
return `/${encodeURIComponent(namespace)}`
}
Expand Down
17 changes: 0 additions & 17 deletions src/commands/mcp/normalize-url.ts

This file was deleted.

8 changes: 4 additions & 4 deletions src/config/command-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function claudeCodeStdioCommand(
/**
* Claude Code HTTP command template
* Generates: claude mcp add --transport http <name> <url>
* Example: claude mcp add --transport http upstash-context-7-mcp "https://server.smithery.ai/@upstash/context7-mcp/mcp"
* Example: claude mcp add --transport http upstash-context-7-mcp "https://mcp.example.com/mcp"
*/
export function claudeCodeHttpCommand(name: string, url: string): string[] {
return ["mcp", "add", "--transport", "http", name, url]
Expand All @@ -41,7 +41,7 @@ export function vscodeStdioCommand(
/**
* VS Code HTTP command template
* Generates: code --add-mcp '{"name":"server","type":"http","url":"https://..."}'
* Example: code --add-mcp '{"name":"upstash-context","type":"http","url":"https://server.smithery.ai/@upstash/context7-mcp/mcp"}'
* Example: code --add-mcp '{"name":"example","type":"http","url":"https://mcp.example.com/mcp"}'
*/
export function vscodeHttpCommand(name: string, url: string): string[] {
return ["--add-mcp", JSON.stringify({ name, type: "http", url })]
Expand All @@ -63,7 +63,7 @@ export function geminiCliStdioCommand(
/**
* Gemini CLI HTTP command template
* Generates: gemini mcp add --transport http <server-name> "<url>"
* Example: gemini mcp add --transport http upstash-context "https://server.smithery.ai/@upstash/context7-mcp/mcp"
* Example: gemini mcp add --transport http example "https://mcp.example.com/mcp"
*/
export function geminiCliHttpCommand(name: string, url: string): string[] {
return ["mcp", "add", "--transport", "http", name, url]
Expand All @@ -85,7 +85,7 @@ export function codexStdioCommand(
/**
* Codex HTTP command template
* Generates: codex mcp add <server-name> --url <url>
* Example: codex mcp add upstash-context --url "https://server.smithery.ai/@upstash/context7-mcp/mcp"
* Example: codex mcp add example --url "https://mcp.example.com/mcp"
*/
export function codexHttpCommand(name: string, url: string): string[] {
return ["mcp", "add", name, "--url", url]
Expand Down
Loading
Loading