# Call tool Source: https://smithery.ai/docs/api-reference/connect/call-tool https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /connect/{namespace}/{connectionId}/.tools/{toolPath} Invoke a tool with JSON arguments. # Create connection Source: https://smithery.ai/docs/api-reference/connect/create-connection https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /connect/{namespace} Create a new MCP connection with an auto-generated ID. Requires API key and namespace ownership. # Create or update connection Source: https://smithery.ai/docs/api-reference/connect/create-or-update-connection https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /connect/{namespace}/{connectionId} Create or update an MCP connection with the given ID. `server` is the Smithery registry qualified name; `mcpUrl` is for custom MCP URLs. One of them is required when creating a new HTTP connection, but optional when updating. Returns 409 if a different target URL is provided, except while the connection is input_required and the new URL keeps the same host and path. # Delete connection Source: https://smithery.ai/docs/api-reference/connect/delete-connection https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /connect/{namespace}/{connectionId} Delete a connection and terminate its MCP session. Requires API key and namespace ownership. # Get connection Source: https://smithery.ai/docs/api-reference/connect/get-connection https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace}/{connectionId} Get details for a specific connection. Requires service token with connections:read scope. # Get tool Source: https://smithery.ai/docs/api-reference/connect/get-tool https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace}/{connectionId}/.tools/{toolPath} Get one tool or list tools under a slash-separated category. # Get trigger Source: https://smithery.ai/docs/api-reference/connect/get-trigger https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace}/{connectionId}/.triggers/{triggerName} Get the schema for a single trigger type. # List connections Source: https://smithery.ai/docs/api-reference/connect/list-connections https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace} List all connections in a namespace. Supports filtering by metadata using `metadata.{key}={value}` query params. # List tools Source: https://smithery.ai/docs/api-reference/connect/list-tools https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace}/{connectionId}/.tools List tools exposed by a connection. # List tools across a namespace Source: https://smithery.ai/docs/api-reference/connect/list-tools-across-a-namespace https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace}/.tools List tools for every connection in a namespace in one response. Each connection is wrapped in an envelope so a failure on one upstream doesn't fail the request. Skips connections in `auth_required`/`input_required` states without calling the upstream. # List triggers Source: https://smithery.ai/docs/api-reference/connect/list-triggers https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /connect/{namespace}/{connectionId}/.triggers List trigger types exposed by a connection. # Subscribe to trigger Source: https://smithery.ai/docs/api-reference/connect/subscribe-to-trigger https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /connect/{namespace}/{connectionId}/.triggers/{triggerName} Subscribe to (or refresh) a trigger. Supplying the same (params, delivery.url) refreshes the TTL and may rotate the secret. # Unsubscribe from trigger Source: https://smithery.ai/docs/api-reference/connect/unsubscribe-from-trigger https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /connect/{namespace}/{connectionId}/.triggers/{triggerName} Unsubscribe by subscription key (params + delivery.url). Eager teardown — subscriptions also expire naturally on TTL. # Health check Source: https://smithery.ai/docs/api-reference/health-check https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /health Check if the service is running # Create a new namespace Source: https://smithery.ai/docs/api-reference/namespaces/create-a-new-namespace https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /namespaces/{name} Create a new namespace owned by the authenticated user or an organization. This endpoint is idempotent - if the namespace already exists and is owned by the user/org, returns success. Pass organizationId in the request body to create an org-owned namespace. # Create a new namespace with generated name Source: https://smithery.ai/docs/api-reference/namespaces/create-a-new-namespace-with-generated-name https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /namespaces Create a new namespace with a server-generated human-readable name, owned by the authenticated user # Create a server under a namespace (deprecated) Source: https://smithery.ai/docs/api-reference/namespaces/create-a-server-under-a-namespace-deprecated https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /namespaces/{namespace}/servers/{server} **Deprecated:** Use PUT /servers/{namespace}/{server} instead. Create a new server under the specified namespace. This endpoint is idempotent. # Delete a namespace Source: https://smithery.ai/docs/api-reference/namespaces/delete-a-namespace https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /namespaces/{name} Delete a namespace owned by the authenticated user. The namespace must not contain any servers. Skills and connections in the namespace will be deleted automatically. # Get user's namespaces or search namespaces Source: https://smithery.ai/docs/api-reference/namespaces/get-users-namespaces-or-search-namespaces https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /namespaces When called without query params, returns the authenticated user's namespaces (backwards compatible). When query params are provided, searches public namespaces with pagination. Use ownerId to filter by owner, hasServers/hasSkills to filter by content, q for text search. # Create a team API key Source: https://smithery.ai/docs/api-reference/organizations/create-a-team-api-key https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /organizations/{orgId}/api-keys Creates an API key owned by the organization. Requires admin role. # List team API keys Source: https://smithery.ai/docs/api-reference/organizations/list-team-api-keys https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /organizations/{orgId}/api-keys Returns all API keys belonging to the organization. Requires admin role. Key values are not included in the response. # Revoke a team API key Source: https://smithery.ai/docs/api-reference/organizations/revoke-a-team-api-key https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /organizations/{orgId}/api-keys/{keyId} Deletes an API key belonging to the organization. Requires admin role. # Create a server Source: https://smithery.ai/docs/api-reference/servers/create-a-server https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /servers/{qualifiedName} Create a new server. Idempotent — returns success if the server already exists and is owned by the caller. # Delete a server Source: https://smithery.ai/docs/api-reference/servers/delete-a-server https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /servers/{qualifiedName} Permanently delete a server, its releases, and associated resources. # Delete server icon Source: https://smithery.ai/docs/api-reference/servers/delete-server-icon https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /servers/{qualifiedName}/icon Remove the server's icon. # Download server bundle Source: https://smithery.ai/docs/api-reference/servers/download-server-bundle https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/download Download the MCPB bundle for the latest successful stdio release. # Get a release Source: https://smithery.ai/docs/api-reference/servers/get-a-release https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/releases/{id} Retrieve release details including status, git metadata, pipeline logs, and MCP endpoint URL. # Get a server Source: https://smithery.ai/docs/api-reference/servers/get-a-server https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName} Retrieve server details including connections, tools, and security status. # Get server icon Source: https://smithery.ai/docs/api-reference/servers/get-server-icon https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/icon Retrieve the server's icon image. Returns the image directly with appropriate content type. # Infer a tool output schema Source: https://smithery.ai/docs/api-reference/servers/infer-a-tool-output-schema https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/experimental/tools/{toolName}/output-schema Infer a best-effort Zod schema from recent successful Tinybird tool outputs. All discovered object fields are marked optional. # List all servers Source: https://smithery.ai/docs/api-reference/servers/list-all-servers https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers Search and browse public MCP servers in the Smithery registry. Supports full-text and semantic search via the `q` parameter, and filtering by deployment status, verification, ownership, and more. # List releases Source: https://smithery.ai/docs/api-reference/servers/list-releases https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/releases List releases ordered by most recent first. Logs are omitted — fetch a specific release to see logs. # List runtime logs Source: https://smithery.ai/docs/api-reference/servers/list-runtime-logs https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/logs Fetch recent runtime logs grouped by invocation. # Publish a server Source: https://smithery.ai/docs/api-reference/servers/publish-a-server https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /servers/{qualifiedName}/releases Submit a release via multipart form. Supports hosted (JS module upload), external (URL), and stdio (MCPB bundle) release types. # Resume a release Source: https://smithery.ai/docs/api-reference/servers/resume-a-release https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /servers/{qualifiedName}/releases/{id}/resume Resume a paused release (e.g. after OAuth authorization). Use id='latest' to resume the most recent one. # Stream release logs Source: https://smithery.ai/docs/api-reference/servers/stream-release-logs https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /servers/{qualifiedName}/releases/{id}/stream Real-time SSE stream of release logs and status updates. # Transfer a server Source: https://smithery.ai/docs/api-reference/servers/transfer-a-server https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /servers/{qualifiedName}/transfer Move a server to another namespace. The caller must have server write access to both the source namespace and the destination namespace. # Update a server Source: https://smithery.ai/docs/api-reference/servers/update-a-server https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml patch /servers/{qualifiedName} Update server metadata such as display name, description, repository, icon, or visibility. # Upload server icon Source: https://smithery.ai/docs/api-reference/servers/upload-server-icon https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /servers/{qualifiedName}/icon Upload or replace the server icon. Accepts a single image file via multipart/form-data. Max 1MB. Supported formats: PNG, JPEG, GIF, SVG, WebP. # Create a new skill (deprecated) Source: https://smithery.ai/docs/api-reference/skills/create-a-new-skill-deprecated https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /skills **Deprecated:** Use PUT /skills/{namespace}/{slug} instead. Create a new skill by linking a GitHub repository containing a SKILL.md file. # Create or update a skill Source: https://smithery.ai/docs/api-reference/skills/create-or-update-a-skill https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml put /skills/{namespace}/{slug} Idempotent endpoint to create or update a GitHub-backed skill. Send application/json with `gitUrl`. # Delete a skill Source: https://smithery.ai/docs/api-reference/skills/delete-a-skill https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml delete /skills/{namespace}/{slug} Delete a skill by namespace and slug. Requires ownership of the namespace. # Get a skill Source: https://smithery.ai/docs/api-reference/skills/get-a-skill https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /skills/{namespace}/{slug} Get a single skill by its namespace and slug. # List or search skills Source: https://smithery.ai/docs/api-reference/skills/list-or-search-skills https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml get /skills Search and browse reusable prompt-based skills. Supports full-text and semantic search via the `q` parameter, and filtering by category, namespace, or slug. # Create a service token Source: https://smithery.ai/docs/api-reference/tokens/create-a-service-token https://app.stainless.com/api/spec/documented/smithery/openapi.documented.yml post /tokens Create a service token for machine-to-machine authentication. Accepts API key or bearer token. Optionally apply restrictions. # Overview Source: https://smithery.ai/docs/build/index Publish your MCP servers on Smithery for distribution and analytics. Smithery helps you distribute your MCP server. When you publish on Smithery, users can discover and connect to your server from any MCP client. ## Why Use Smithery? * **Distribution** — Get a dedicated server page where users can discover, try, and install your MCP from any client * **Analytics** — Track tool calls, monitor usage patterns, and understand how users interact with your server * **Spec Compliance** — The Gateway handles MCP protocol compliance, metadata enrichment, and caching automatically * **OAuth UI** — Automatic authentication modal generation for servers that require user configuration or API keys ## Already Hosting Your Server? If you've already deployed an MCP server elsewhere, you can publish it directly on Smithery via the [URL method](/docs/build/publish). Any server exposing Streamable HTTP is compatible. If you have an MCPB bundle for a local stdio server, you can publish that artifact on Smithery too. ## Getting Started Distribute your server via URL or Local. # Publish Source: https://smithery.ai/docs/build/publish Publish your MCP server on Smithery for distribution and analytics. Publish your MCP server to Smithery for distribution, analytics, and configuration UI. **Bring your own hosting** — Smithery Gateway proxies to your upstream server. 1. Go to [smithery.ai/new](https://smithery.ai/new) 2. Enter your server's public HTTPS URL 3. Complete the publishing flow ### Requirements * Streamable HTTP transport * OAuth support (if auth required) **No client registration needed.** Smithery handles client registration automatically via [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/draft/basic/authorization#client-id-metadata-documents). **Need a framework or hosting?** Build MCP servers with [xmcp](https://xmcp.dev) or host them on [Gram](https://www.getgram.ai/) — both work with Smithery's URL publishing. ### Server Scanning Smithery scans your server to extract metadata (tools, prompts, resources) for your server page. * **Public servers**: Scan completes automatically * **Auth-required servers**: You'll be prompted to authenticate so we can complete the scan If automatic scanning can't complete (auth wall, required configuration, or other issues), you can provide server metadata manually via a static server card at `/.well-known/mcp/server-card.json`: ```json theme={null} { "serverInfo": { "name": "Your Server Name", "version": "1.0.0" }, "authentication": { "required": true, "schemes": ["oauth2"] }, "tools": [ { "name": "search", "description": "Search for information", "inputSchema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] } } ], "resources": [], "prompts": [] } ``` **Fields:** * `serverInfo` (required): Server name and version * `authentication` (optional): Auth requirements and supported schemes * `tools`, `resources`, `prompts` (optional): Capability definitions per MCP spec The schema follows types from [`@modelcontextprotocol/sdk/types.js`](https://github.com/modelcontextprotocol/typescript-sdk). See [SEP-1649](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1649) for the spec proposal. You can also publish a URL-based server via CLI with a custom config schema: ```bash theme={null} smithery mcp publish "https://your-server.com/mcp" -n @your-org/your-server ``` To specify a config schema, pass it as a JSON string: ```bash theme={null} smithery mcp publish "https://your-server.com/mcp" -n @your-org/your-server --config-schema '{"type":"object","properties":{"apiKey":{"type":"string"}}}' ``` See [Session Configuration](/docs/build/session-config) for JSON Schema format with `x-from` extension. **For local stdio servers** — Smithery distributes a pre-built MCPB bundle that clients download and run locally. 1. Prepare your `.mcpb` bundle 2. Publish the bundle to Smithery 3. Complete the publishing flow For MCPB authoring guidance, see Anthropic's [Build a desktop extension with MCPB](https://claude.com/docs/connectors/building/mcpb) guide and the [MCPB specification](https://github.com/modelcontextprotocol/mcpb). ### What gets published * `server.mcpb` — the MCPB bundle distributed to clients * Configuration schema and metadata used to render your Smithery server page * The latest downloadable stdio artifact for local installation Smithery accepts multipart `bundle` uploads for stdio releases. See [Publish a server](/docs/api-reference/servers/publish-a-server). Publish a bundle with: ```bash theme={null} smithery mcp publish ./server.mcpb -n your-org/your-server ``` ## Troubleshooting ### 403 Forbidden during scan If your deployment fails with **"Initialization failed with status 403"**, it means your server rejected Smithery's scan request. Common causes: * **WAF or bot protection** (e.g. Cloudflare Bot Fight Mode) blocking automated requests * **Server returning 403 for unauthenticated requests** instead of 401 — per the [MCP auth spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization), servers should return 401 to trigger OAuth discovery * **IP-based access restrictions** or allowlists that don't include Smithery's IP range Smithery sends requests with User-Agent `SmitheryBot/1.0 (+https://smithery.ai)`. These requests originate from Cloudflare Workers, which some WAF configurations block by default. #### Option 1: Ensure your server returns 401 (not 403) for OAuth If your server requires OAuth, make sure it returns **401 Unauthorized** (not 403 Forbidden) for unauthenticated requests. Smithery uses the 401 response to detect OAuth support per [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728.html). #### Option 2: Whitelist Smithery requests Bot Fight Mode on the free plan cannot be bypassed with WAF custom rules. Your options: 1. **IP Access Rules**: Go to **Security > WAF > Tools > IP Access Rules** and add an Allow rule for Smithery's IP range 2. **Disable Bot Fight Mode**: Go to **Security > Bots > Bot Fight Mode** and toggle it off (this disables bot protection for all traffic) 3. **Upgrade to Pro**: Pro plan (\$20/mo) unlocks Super Bot Fight Mode with WAF skip rules (see below) Create a WAF skip rule to bypass bot protection for Smithery: 1. Go to **Security > WAF > Custom Rules** 2. Create a rule with expression: `(http.user_agent contains "SmitheryBot")` 3. Action: **Skip** > select **Super Bot Fight Mode** Add an allow rule for requests matching User-Agent `SmitheryBot/1.0`. The exact steps vary by provider — consult your CDN/WAF documentation for configuring User-Agent-based allow rules. #### Option 3: Publish a static server card Bypass scanning entirely by serving a `/.well-known/mcp/server-card.json` endpoint on your server. See [Static Server Card](#server-scanning) above. ## Get verified Once your server is published, open the server's **Settings → Verification** page to complete the automatic official-vendor verification checklist. # Triggers Source: https://smithery.ai/docs/build/triggers Expose events from your MCP server so consumers can receive them as webhooks. Triggers are in preview. Breaking changes may happen without notice. Triggers let an MCP server surface events from its upstream service. When a consumer activates a trigger via Smithery, Smithery proxies the subscribe call straight through to your server with the consumer's webhook URL and signing secret. When the upstream provider fires, your server POSTs the event **directly to the consumer** — Smithery is not in the delivery path. Your server is not in the hot path for events — only for setup and teardown. This keeps trigger support compatible with serverless runtimes. ## Alignment with the MCP Events proposal This extension tracks the webhook slice of the MCP committee's [events proposal](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/pull/1). Smithery is intentionally minimal: it implements only what the spec requires for webhook delivery, and skips optional surface area while the proposal is still draft. What Smithery implements today: * `events/list` — full parity * `events/subscribe` in **webhook** mode only, with mandatory TTL refresh * `events/unsubscribe` What Smithery does not implement: * `poll` and `push` (`events/stream`) delivery modes * Cursor replay — events start "from now" * `truncated`, `maxAge`, `maxEvents` parameters * Control envelopes (`gap`, `terminated`) * `deliveryStatus` reporting * Asymmetric endpoint verification or server signing Smithery acts as the spec's "client" against your server: it forwards the consumer's `delivery.url` and Standard Webhooks `delivery.secret` unchanged. Your server signs deliveries with that secret and POSTs them to that URL. ## How it works ```mermaid theme={null} sequenceDiagram participant App participant Smithery participant MCP Server participant Upstream App->>Smithery: ai.smithery/events/subscribe (params, delivery) Smithery->>MCP Server: ai.smithery/events/subscribe (params, delivery) MCP Server->>Upstream: register webhook MCP Server-->>Smithery: { id, refreshBefore } Smithery-->>App: { id, refreshBefore } Note over App,Smithery: App re-POSTs subscribe before
refreshBefore to keep alive Upstream->>MCP Server: event fires MCP Server->>App: POST app's URL (signed with delivery.secret) App-->>MCP Server: 2xx ``` 1. The consumer creates a trigger through Smithery, supplying their own webhook URL and Standard Webhooks secret. 2. Smithery, authenticated as the connection's principal, calls your `ai.smithery/events/subscribe` and forwards the consumer's `delivery.url` and `delivery.secret` unchanged. 3. Your server uses the connection's credentials to register an upstream webhook (or start whatever event source it needs) and returns `{ id, refreshBefore }`. 4. Before `refreshBefore`, the consumer re-POSTs through Smithery, which re-calls subscribe with the same key — your server treats this as an idempotent upsert and resets the TTL. 5. When the upstream fires, your server POSTs the event **directly to the consumer's `delivery.url`**, signed with `delivery.secret`. Smithery is not involved in delivery. 6. When the consumer deletes the trigger (or stops refreshing), Smithery calls `ai.smithery/events/unsubscribe` to tear down the upstream registration. ## Negotiation Advertise the extension in your `initialize` response: ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25", "capabilities": { "extensions": { "ai.smithery/events": {} } }, "serverInfo": { "name": "NotionMCP", "version": "1.0.0" } } } ``` ## Methods ### `ai.smithery/events/list` Return the catalog of event types your server supports. Each entry declares the params a subscriber must provide and the payload your server will deliver. ```json theme={null} { "jsonrpc": "2.0", "id": 2, "result": { "events": [ { "name": "page.updated", "description": "Fires when a page in the watched workspace is updated.", "delivery": ["webhook"], "inputSchema": { "type": "object", "properties": { "workspace_id": { "type": "string" } }, "required": ["workspace_id"] }, "payloadSchema": { "type": "object", "properties": { "page_id": { "type": "string" }, "updated_at": { "type": "string", "format": "date-time" } } } } ] } } ``` | Field | Type | Description | | --------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Unique event name scoped to your server. | | `description` | `string` | Human-readable summary of when this event fires. | | `delivery` | `string[]` | Delivery modes this event supports. For Smithery, return `["webhook"]`. | | `inputSchema` | `object` | JSON Schema for the `params` a subscriber must supply (e.g. workspace or channel scoping). Mirrors the `inputSchema` convention from tools. | | `payloadSchema` | `object` | JSON Schema describing the `data` object your server emits. | ### `ai.smithery/events/subscribe` Smithery calls this when a consumer activates a trigger, and again before each `refreshBefore` to keep the subscription alive. Register an upstream webhook pointing at `delivery.url`, retain `delivery.secret` to sign each POST, and return a stable subscription `id` plus a fresh `refreshBefore`. Request: ```json theme={null} { "jsonrpc": "2.0", "id": 3, "method": "ai.smithery/events/subscribe", "params": { "name": "page.updated", "params": { "workspace_id": "w_123" }, "delivery": { "mode": "webhook", "url": "https://my-app.example.com/events", "secret": "whsec_" } } } ``` Response: ```json theme={null} { "jsonrpc": "2.0", "id": 3, "result": { "id": "sub_a3f1c8e2b0d49f7e", "refreshBefore": "2026-04-22T13:00:00.000Z" } } ``` | Param | Description | | ----------------- | ---------------------------------------------------------------------------------------------------- | | `name` | The event name from `events/list`. | | `params` | Subscriber-supplied arguments conforming to the event's `inputSchema`. | | `delivery.mode` | Always `"webhook"` for this extension. | | `delivery.url` | The consumer's HTTPS webhook endpoint. POSTs go here directly. | | `delivery.secret` | The consumer's Standard Webhooks `whsec_` secret. Sign each POST to `delivery.url` with this secret. | | Result field | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Stable subscription handle deterministic over `(principal, name, params, delivery.url)`. The receiver uses it to look up the right secret via the `X-MCP-Subscription-Id` header. | | `refreshBefore` | ISO 8601 timestamp at which the subscription expires unless refreshed. Pick a TTL that reflects how long you can hold soft state for (e.g. 30 minutes). | #### Idempotent upsert and TTL refresh `ai.smithery/events/subscribe` is keyed by `(principal, name, params, delivery.url)`. If a subscription with the same key already exists, treat the call as an upsert: * **TTL** — reset, return a new `refreshBefore`. * **`delivery.secret`** — replace. During rotation, the spec recommends dual-signing with the old and new secrets for a short grace window using Standard Webhooks' multi-signature support. * **`id`** — unchanged. If the subscription has expired (or your server lost it on restart), create a fresh one for the same key and return a new `id`. If Smithery stops refreshing before `refreshBefore`, expire the subscription and tear down the upstream webhook — no explicit unsubscribe is required. ### `ai.smithery/events/unsubscribe` Smithery calls this on eager teardown (consumer deleted the trigger). Tear down the upstream registration you created. ```json theme={null} { "jsonrpc": "2.0", "id": 4, "method": "ai.smithery/events/unsubscribe", "params": { "name": "page.updated", "params": { "workspace_id": "w_123" }, "delivery": { "url": "https://my-app.example.com/events" } } } ``` Response: ```json theme={null} { "jsonrpc": "2.0", "id": 4, "result": {} } ``` Look up the subscription by `(principal, name, params, delivery.url)` and deregister the corresponding upstream webhook. The subscription `id` is not accepted here — the key is the canonical identifier. ## Event delivery When an upstream event arrives, POST it to `delivery.url`, signed with the Standard Webhooks headers and the consumer's `delivery.secret`. The consumer verifies the signature and processes the event. You are responsible for: 1. Declaring a `payloadSchema` that accurately describes the `data` field. 2. Configuring upstream webhook registration so the payload delivered to the consumer matches that schema. 3. Never including secrets or PII the consumer shouldn't see. If the upstream payload requires transformation (unwrapping envelopes, hydrating IDs, redacting fields), do it at registration time — use the provider's webhook filter or payload-shape features. The body sent to the consumer is an `EventOccurrence`: ```json theme={null} { "eventId": "evt_01HW...", "name": "page.updated", "timestamp": "2026-04-22T12:00:00.000Z", "data": { "page_id": "...", "updated_at": "2026-04-22T12:00:00.000Z" } } ``` | Field | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `eventId` | Stable event identifier. Use the upstream's stable id (Stripe `evt_*`, GitHub delivery GUID, Gmail message id) so the receiver can dedupe across retries. | | `name` | The event name from `events/list`. | | `timestamp` | ISO 8601 timestamp of the upstream event. | | `data` | Payload conforming to the event's `payloadSchema`. | ### Webhook signing Sign every POST with the [Standard Webhooks](https://www.standardwebhooks.com/) HMAC profile. Required headers: | Header | Value | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `webhook-id` | Stable event id for deduplication | | `webhook-timestamp` | Unix seconds | | `webhook-signature` | `v1,` | | `X-MCP-Subscription-Id` | The subscription `id` you returned from `events/subscribe`. The receiver uses this to look up the right secret before parsing the body. | Sign the raw request body with a Standard Webhooks library. Each retry attempt MUST regenerate the timestamp and signature so receivers do not reject a later attempt as stale. ## API reference ### Extension capability Advertise during `initialize` under `capabilities.extensions`: ```json theme={null} { "ai.smithery/events": {} } ``` ### MCP methods (Smithery → server) | Method | Params | Result | | -------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | | `ai.smithery/events/list` | — | `{ events: [{ name, description, delivery, inputSchema, payloadSchema }] }` | | `ai.smithery/events/subscribe` | `{ name, params, delivery: { mode, url, secret } }` | `{ id, refreshBefore }` | | `ai.smithery/events/unsubscribe` | `{ name, params, delivery: { url } }` | `{}` | ## Learn more * [Using triggers](/docs/use/triggers) — consumer-facing docs * [MCP Events proposal (PR #1)](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/pull/1) * [Publish a server](/docs/build/publish) * [Standard Webhooks spec](https://www.standardwebhooks.com/) # Smithery CLI Source: https://smithery.ai/docs/concepts/cli Use the Smithery CLI to search, connect, and manage MCP servers and skills from the command line. The Smithery CLI connects your agents to thousands of skills and MCP servers directly from the command line. ## Installation ```bash theme={null} npm install -g smithery@latest ``` Requires Node.js 20+. ## Examples ```bash theme={null} # Show the help menu smithery --help # Authenticate with Smithery smithery auth login # Search for MCP servers smithery mcp search "github" # Add an MCP server to a local client (e.g., Claude Desktop) smithery mcp add exa --client claude # Add an MCP server as a remote Smithery connection smithery mcp add https://server.smithery.ai/exa --id exa # List tools from your connected MCP servers smithery tool list # Call a tool smithery tool call exa search '{"query": "latest news about MCP"}' # Search and add skills smithery skill search "code review" smithery skill add anthropics/frontend-design --agent claude-code ``` ## Reference ### MCP Servers ```bash theme={null} smithery mcp search [term] # Search the Smithery registry smithery mcp add # Add an MCP connection (remote by default) smithery mcp add --client # Add to a local client (e.g., claude, cursor) smithery mcp list # List your connections smithery mcp remove # Remove connections smithery mcp get # Get connection details smithery mcp update # Update a connection smithery mcp publish -n # Publish a URL-based MCP server smithery mcp publish -n # Publish an MCPB bundle ``` ### Tools Interact with tools from MCP servers connected via `smithery mcp`. ```bash theme={null} smithery tool list [connection] # List tools from your connected MCP servers smithery tool find [query] # Search tools by name or intent smithery tool get # Show full details for one tool smithery tool call [args] # Call a tool ``` ### Skills Browse and add skills from the [Smithery Skills Registry](https://smithery.ai/skills). ```bash theme={null} smithery skill search [query] # Search skills smithery skill add --agent # Add a skill ``` ### Auth ```bash theme={null} smithery auth login # Login with Smithery (OAuth) smithery auth logout # Log out smithery auth whoami # Check current user smithery auth token # Mint a service token ``` ### Namespaces ```bash theme={null} smithery namespace list # List your namespaces smithery namespace use # Set current namespace ``` ### Global Flags * `--json` - Output as JSON (auto-detected in non-TTY environments) * `--table` - Output as table * `--verbose` - Show detailed logs for debugging * `--help` - Show help message ### Examples ```bash theme={null} # Add an MCP server to Claude Desktop smithery mcp add mcp-obsidian --client claude # Add with pre-configured data (skips prompts) smithery mcp add mcp-obsidian --client claude --config '{"vaultPath":"path/to/vault"}' # Remove a server from a client smithery mcp remove mcp-obsidian --client claude # Search for MCP servers with JSON output smithery --json mcp search "database" # List tools from a specific connection smithery tool list my-github # Find tools by intent smithery tool find "create issue" # Call a tool with JSON arguments smithery tool call my-github create_issue '{"title":"Bug fix","body":"..."}' # Login and check auth smithery auth login smithery auth whoami # Publish your MCP server smithery mcp publish "https://my-server.com" -n myorg/my-server # Publish an MCPB bundle smithery mcp publish ./server.mcpb -n myorg/my-server # Show help smithery --help ``` ### Important Notes * Use `auth login` to authenticate with Smithery (required for some operations) * Remember to restart your AI client after adding or removing servers * Use `--verbose` flag for detailed logs when troubleshooting * Use `--json` flag for machine-readable output * `mcp publish` accepts either a public MCP URL or a local `.mcpb` bundle # Namespaces Source: https://smithery.ai/docs/concepts/namespaces Namespaces group your servers, connections, and skills under a shared identifier. # Namespaces A namespace is a container for organizing your resources on Smithery. Every server, connection, and skill you create belongs to a namespace. Namespaces appear in URLs and qualified names. For example, a server published under the `acme` namespace with the slug `weather` is accessible at `smithery.ai/servers/acme/weather`. ## What belongs to a namespace? | Resource | Description | | ----------------------------------------- | --------------------------------------------------------- | | **[Servers](/docs/api-reference/servers)** | MCP servers you publish to the Smithery registry | | **[Connections](/docs/api-reference/connect)** | Managed MCP connections with OAuth and credential storage | | **[Skills](/docs/api-reference/skills)** | Reusable AI skills that reference MCP servers | ## Creating a namespace You can create namespaces via the [Namespaces API](/docs/api-reference/namespaces): ```bash theme={null} smithery namespace create my-namespace ``` ```typescript theme={null} import Smithery from '@smithery/api' const smithery = new Smithery() // Specific name await smithery.namespaces.set('my-namespace') // Or auto-generated name const namespace = await smithery.namespaces.create() ``` ```bash theme={null} # Auto-generated name curl -X POST https://api.smithery.ai/namespaces \ -H "Authorization: Bearer YOUR_API_KEY" # Specific name curl -X PUT https://api.smithery.ai/namespaces/my-namespace \ -H "Authorization: Bearer YOUR_API_KEY" ``` Namespace names must be lowercase alphanumeric with hyphens, and are globally unique across Smithery. ## Limits | Plan | Max namespaces | | ------------- | -------------- | | Hobby (free) | 3 | | Pay as you Go | 100 | | Custom | 100+ | See the [pricing page](https://smithery.ai/pricing) for full plan details. ## Sharing and collaboration Namespaces act as a shared workspace. Resources within a namespace can be managed by anyone with the appropriate [API key](/docs/use/token-scoping) scoped to that namespace, making them suitable for team collaboration similar to project IDs. # What is MCP? Source: https://smithery.ai/docs/concepts/what_is_mcp What is MCP? How does it work? What does Smithery do with MCP? MCP Diagram # What is MCP? MCP (Model Context Protocol) is an open standard that enables LLMs to access bespoke tools and context, as long as the LLM supports MCP and the tool implements the MCP protocol. ### Use Cases Broadly speaking, MCP is useful when you want your LLM to get information from the outside world or perform actions on your behalf: * **Context**: Context is data that can be used by the LLM that may not be available in the LLM's training data. For example, your LLM might want to search the web or access internal company data. * **Tools**: Tools are functions that can be called by the LLM. This can be something like "open a pull request from one branch into main" or "send an email to a specific person". ### Servers, Clients, and Transports MCP, just like HTTP, is a client-server protocol. The **client** is the LLM, and the **server** exposes tools to the LLM. The client and server communicate over a **transport**, which can be fully local or over the internet. * **Servers** expose tools to a client. * **Clients** can call tools on a server. * **Transports** are how the client and server communicate with each other. This can be local (STDIO), over the internet (HTTP), or even over a custom transport. Learn more about MCP from the Model Context Protocol website (modelcontextprotocol.io) # What does Smithery do with MCP? Smithery is a platform that connects MCP servers with users. We allow users to find the right MCP server for their needs, and we allow developers to publish MCP servers for distribution. We have a diverse mix of MCP servers, both local (STDIO) and over the internet (HTTP). Here are some popular ones: * [Desktop Commander](https://smithery.ai/servers/@wonderwhy-er/desktop-commander) (Local, STDIO): Allow apps like Claude Desktop to run terminal commands on your computer. * [Exa](https://smithery.ai/servers/exa) (Remote, HTTP): Access live web search results. Do Deep Research, get LinkedIn profiles, and more! * [Context7](https://smithery.ai/servers/@upstash/context7-mcp) (Remote, HTTP): Let your IDE agent like Cursor Composer access documentation for your favorite libraries and SDKs. * [Browserbase](https://smithery.ai/servers/@browserbasehq/mcp-browserbase) (Remote, HTTP): Use Claude Desktop/Cursor to control a remote web browser. You can also publish your own MCP servers on Smithery! Learn how to publish your MCP server to Smithery for distribution and observability # Build an OAuth-compatible client Source: https://smithery.ai/docs/cookbooks/typescript_oauth_client How to build an OAuth client in Typescript using Next.js This guide will walk you through the process of building an OAuth-compatible MCP client in Typescript. We'll be using Next.js and the Model Context Protocol SDK. It builds off of the [official example from the MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/client/simpleOAuthClient.ts). View the fully runnable GitHub repo for this example View the official MCP docs for authorization ## 1. Install dependencies First, create a new Next.js project and install the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk). ```bash theme={null} npx create-next-app@latest my-mcp-client cd my-mcp-client npm install @modelcontextprotocol/sdk ``` ## 2. Create Library Files You'll want to create a `/lib` directory for common code across your application. This will contain the core logic for the MCP client and the authentication flow. Then, create the following files in your `/lib` directory: ### Session Store This file contains a simple in-memory session store. For production applications, you should use a more robust solution like Redis or a database. ```typescript /lib/session-store.ts expandable theme={null} import { MCPOAuthClient } from "./oauth-client"; // Simple in-memory session store for demo purposes // In production, use Redis, database, or proper session management class SessionStore { private clients = new Map(); setClient(sessionId: string, client: MCPOAuthClient) { this.clients.set(sessionId, client); } getClient(sessionId: string): MCPOAuthClient | null { return this.clients.get(sessionId) || null; } removeClient(sessionId: string) { const client = this.clients.get(sessionId); if (client) { client.disconnect(); this.clients.delete(sessionId); } } generateSessionId(): string { return Math.random().toString(36).substring(2) + Date.now().toString(36); } } export const sessionStore = new SessionStore(); ``` ### OAuth Client This file contains the core logic for the MCP OAuth client. ```typescript /lib/oauth-client.ts expandable theme={null} import { URL } from "node:url"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens, } from "@modelcontextprotocol/sdk/shared/auth.js"; import { CallToolRequest, ListToolsRequest, CallToolResultSchema, ListToolsResultSchema, ListToolsResult, CallToolResult, } from "@modelcontextprotocol/sdk/types.js"; import { OAuthClientProvider, UnauthorizedError, } from "@modelcontextprotocol/sdk/client/auth.js"; class InMemoryOAuthClientProvider implements OAuthClientProvider { private _clientInformation?: OAuthClientInformationFull; private _tokens?: OAuthTokens; private _codeVerifier?: string; constructor( private readonly _redirectUrl: string | URL, private readonly _clientMetadata: OAuthClientMetadata, onRedirect?: (url: URL) => void ) { this._onRedirect = onRedirect || ((url) => { console.log(`Redirect to: ${url.toString()}`); }); } private _onRedirect: (url: URL) => void; get redirectUrl(): string | URL { return this._redirectUrl; } get clientMetadata(): OAuthClientMetadata { return this._clientMetadata; } clientInformation(): OAuthClientInformation | undefined { return this._clientInformation; } saveClientInformation(clientInformation: OAuthClientInformationFull): void { this._clientInformation = clientInformation; } tokens(): OAuthTokens | undefined { return this._tokens; } saveTokens(tokens: OAuthTokens): void { this._tokens = tokens; } redirectToAuthorization(authorizationUrl: URL): void { this._onRedirect(authorizationUrl); } saveCodeVerifier(codeVerifier: string): void { this._codeVerifier = codeVerifier; } codeVerifier(): string { if (!this._codeVerifier) { throw new Error("No code verifier saved"); } return this._codeVerifier; } } export class MCPOAuthClient { private client: Client | null = null; private oauthProvider: InMemoryOAuthClientProvider | null = null; constructor( private serverUrl: string, private callbackUrl: string, private onRedirect: (url: string) => void ) {} async connect(): Promise { const clientMetadata: OAuthClientMetadata = { client_name: "Next.js MCP OAuth Client", redirect_uris: [this.callbackUrl], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "client_secret_post", scope: "mcp:tools", }; this.oauthProvider = new InMemoryOAuthClientProvider( this.callbackUrl, clientMetadata, (redirectUrl: URL) => { this.onRedirect(redirectUrl.toString()); } ); this.client = new Client( { name: "nextjs-oauth-client", version: "1.0.0", }, { capabilities: {} } ); await this.attemptConnection(); } private async attemptConnection(): Promise { if (!this.client || !this.oauthProvider) { throw new Error("Client not initialized"); } const baseUrl = new URL(this.serverUrl); const transport = new StreamableHTTPClientTransport(baseUrl, { authProvider: this.oauthProvider, }); try { await this.client.connect(transport); } catch (error) { if (error instanceof UnauthorizedError) { throw new Error("OAuth authorization required"); } else { throw error; } } } async finishAuth(authCode: string): Promise { if (!this.client || !this.oauthProvider) { throw new Error("Client not initialized"); } const baseUrl = new URL(this.serverUrl); const transport = new StreamableHTTPClientTransport(baseUrl, { authProvider: this.oauthProvider, }); await transport.finishAuth(authCode); await this.client.connect(transport); } async listTools(): Promise { if (!this.client) { throw new Error("Not connected to server"); } const request: ListToolsRequest = { method: "tools/list", params: {}, }; return await this.client.request(request, ListToolsResultSchema); } async callTool( toolName: string, toolArgs: Record ): Promise { if (!this.client) { throw new Error("Not connected to server"); } const request: CallToolRequest = { method: "tools/call", params: { name: toolName, arguments: toolArgs, }, }; return await this.client.request(request, CallToolResultSchema); } disconnect(): void { this.client = null; this.oauthProvider = null; } } ``` ## 3. Create OAuth API Routes Next, you'll want to create a few API routes to handle the OAuth flow. Create the following API routes in a directory at `/app/api/mcp`: * `/app/api/mcp/auth/connect` - Initiates the connection to the MCP server. * `/app/api/mcp/auth/callback` - Handles the OAuth callback from the MCP server. * `/app/api/mcp/auth/finish` - Finalizes the OAuth flow and stores the tokens. * `/app/api/mcp/auth/disconnect` - Disconnects from the MCP server. ### Initialize the OAuth flow This endpoint initiates the connection to the MCP server. ```typescript /app/api/mcp/auth/connect/route.ts expandable theme={null} import { NextRequest, NextResponse } from "next/server"; import { MCPOAuthClient } from "@/lib/oauth-client"; import { sessionStore } from "@/lib/session-store"; interface ConnectRequestBody { serverUrl: string; callbackUrl: string; } export async function POST(request: NextRequest) { try { const body: ConnectRequestBody = await request.json(); const { serverUrl, callbackUrl } = body; if (!serverUrl || !callbackUrl) { return NextResponse.json( { error: "Server URL and callback URL are required" }, { status: 400 } ); } const sessionId = sessionStore.generateSessionId(); let authUrl: string | null = null; const client = new MCPOAuthClient( serverUrl, callbackUrl, (redirectUrl: string) => { authUrl = redirectUrl; } ); try { await client.connect(); // If we get here, connection succeeded without OAuth sessionStore.setClient(sessionId, client); return NextResponse.json({ success: true, sessionId }); } catch (error: unknown) { if (error instanceof Error) { if (error.message === "OAuth authorization required" && authUrl) { // Store client for later use sessionStore.setClient(sessionId, client); return NextResponse.json( { requiresAuth: true, authUrl, sessionId }, { status: 401 } ); } else { return NextResponse.json( { error: error.message || "Unknown error" }, { status: 500 } ); } } } } catch (error: unknown) { if (error instanceof Error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ error: String(error) }, { status: 500 }); } } ``` ### Handle the OAuth callback This is the OAuth callback endpoint. ```typescript /app/api/mcp/auth/callback/route.ts expandable theme={null} import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const code = searchParams.get("code"); const error = searchParams.get("error"); if (code) { const html = `

Authorization Successful!

You can close this window and return to the app.

`; return new NextResponse(html, { headers: { "Content-Type": "text/html" }, }); } else if (error) { const html = `

Authorization Failed

Error: ${error}

`; return new NextResponse(html, { headers: { "Content-Type": "text/html" }, }); } return new NextResponse("Bad request", { status: 400 }); } ``` ### Finalize the OAuth flow This endpoint finalizes the OAuth flow. ```typescript /app/api/mcp/auth/finish/route.ts expandable theme={null} import { NextRequest, NextResponse } from "next/server"; import { sessionStore } from "@/lib/session-store"; interface FinishAuthRequestBody { authCode: string; sessionId: string; } export async function POST(request: NextRequest) { try { const body: FinishAuthRequestBody = await request.json(); const { authCode, sessionId } = body; if (!authCode || !sessionId) { return NextResponse.json( { error: "Authorization code and session ID are required" }, { status: 400 } ); } const client = sessionStore.getClient(sessionId); if (!client) { return NextResponse.json( { error: "No active OAuth session found" }, { status: 400 } ); } await client.finishAuth(authCode); return NextResponse.json({ success: true }); } catch (error: unknown) { if (error instanceof Error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ error: String(error) }, { status: 500 }); } } ``` ### Disconnect from the MCP server This endpoint disconnects from the MCP server. ```typescript /app/api/mcp/auth/disconnect/route.ts expandable theme={null} import { NextRequest, NextResponse } from "next/server"; import { sessionStore } from "@/lib/session-store"; interface DisconnectRequestBody { sessionId: string; } export async function POST(request: NextRequest) { try { const body: DisconnectRequestBody = await request.json(); const { sessionId } = body; if (!sessionId) { return NextResponse.json( { error: "Session ID is required" }, { status: 400 } ); } sessionStore.removeClient(sessionId); return NextResponse.json({ success: true }); } catch (error: unknown) { if (error instanceof Error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ error: String(error) }, { status: 500 }); } } ``` ## 4. List/Call Tools Next, you'll want to create a few API routes to handle actually using the tools provided by the MCP server. Create the following API routes in a directory at `/app/api/mcp`: * `/app/api/mcp/tool/list` - Lists the available tools on the MCP server. * `/app/api/mcp/tool/call` - Calls a tool on the MCP server. ### List the available tools This endpoint lists the available tools on the MCP server. ```typescript /app/api/mcp/tool/list/route.ts expandable theme={null} import { NextRequest, NextResponse } from "next/server"; import { sessionStore } from "@/lib/session-store"; export async function GET(request: NextRequest) { try { const sessionId = request.nextUrl.searchParams.get("sessionId"); if (!sessionId) { return NextResponse.json( { error: "Session ID is required" }, { status: 400 } ); } const client = sessionStore.getClient(sessionId); if (!client) { return NextResponse.json( { error: "Not connected to server" }, { status: 400 } ); } const result = await client.listTools(); return NextResponse.json({ tools: result.tools || [] }); } catch (error: unknown) { if (error instanceof Error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ error: String(error) }, { status: 500 }); } } ``` ### Call a tool This endpoint calls a tool on the MCP server. ```typescript /app/api/mcp/tool/call/route.ts expandable theme={null} import { NextRequest, NextResponse } from "next/server"; import { sessionStore } from "@/lib/session-store"; interface CallToolRequestBody { toolName: string; toolArgs?: Record; sessionId: string; } export async function POST(request: NextRequest) { try { const body: CallToolRequestBody = await request.json(); const { toolName, toolArgs, sessionId } = body; if (!toolName || !sessionId) { return NextResponse.json( { error: "Tool name and session ID are required" }, { status: 400 } ); } const client = sessionStore.getClient(sessionId); if (!client) { return NextResponse.json( { error: "Not connected to server" }, { status: 400 } ); } const result = await client.callTool(toolName, toolArgs || {}); return NextResponse.json({ result }); } catch (error: unknown) { if (error instanceof Error) { return NextResponse.json({ error: error.message }, { status: 500 }); } return NextResponse.json({ error: String(error) }, { status: 500 }); } } ``` ## 5. Build the UI Now, you'll want to build the UI to call these endpoints. You can use the following code to get started: ```typescript /app/page.tsx expandable theme={null} "use client"; import { useState } from "react"; interface SchemaProperty { type?: string; description?: string; default?: unknown; } interface Tool { name: string; description?: string; inputSchema?: { type: "object"; properties?: Record; required?: string[]; }; } export default function Home() { const [serverUrl, setServerUrl] = useState( "https://exa.run.tools" ); const [sessionId, setSessionId] = useState(null); const [isConnected, setIsConnected] = useState(false); const [tools, setTools] = useState([]); const [selectedTool, setSelectedTool] = useState(""); const [toolArgs, setToolArgs] = useState("{}"); const [toolResult, setToolResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [jsonError, setJsonError] = useState(null); const generateDefaultArgs = (tool: Tool): string => { if (!tool.inputSchema?.properties) { return "{}"; } const defaultArgs: Record = {}; Object.entries(tool.inputSchema.properties).forEach(([key, schema]) => { switch (schema.type) { case "string": defaultArgs[key] = schema.default || ""; break; case "number": case "integer": defaultArgs[key] = schema.default || 0; break; case "boolean": defaultArgs[key] = schema.default || false; break; case "array": defaultArgs[key] = schema.default || []; break; case "object": defaultArgs[key] = schema.default || {}; break; default: defaultArgs[key] = schema.default || null; } }); return JSON.stringify(defaultArgs, null, 2); }; const handleToolSelect = (toolName: string) => { setSelectedTool(toolName); setJsonError(null); if (toolName) { const tool = tools.find((t) => t.name === toolName); if (tool) { setToolArgs(generateDefaultArgs(tool)); } } else { setToolArgs("{}"); } }; const handleArgsChange = (value: string) => { setToolArgs(value); // Validate JSON syntax try { if (value.trim()) { JSON.parse(value); } setJsonError(null); } catch (e) { setJsonError(e instanceof Error ? e.message : "Invalid JSON"); } }; const getCallbackUrl = () => { return `${window.location.origin}/api/mcp/auth/callback`; }; const handleConnect = async () => { if (!serverUrl) return; setLoading(true); setError(null); setJsonError(null); try { const response = await fetch("/api/mcp/auth/connect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ serverUrl, callbackUrl: getCallbackUrl() }), }); const data = await response.json(); if (!response.ok) { if (data.requiresAuth && data.authUrl && data.sessionId) { setSessionId(data.sessionId); // Open authorization URL in a popup const popup = window.open( data.authUrl, "oauth-popup", "width=600,height=700,scrollbars=yes,resizable=yes" ); // Listen for messages from the popup const messageHandler = async (event: MessageEvent) => { if (event.origin !== window.location.origin) return; if (event.data.type === "oauth-success") { popup?.close(); try { const finishResponse = await fetch("/api/mcp/auth/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ authCode: event.data.code, sessionId: data.sessionId, }), }); if (finishResponse.ok) { setIsConnected(true); await loadTools(data.sessionId); } else { const errorData = await finishResponse.json(); setError( `Failed to complete authentication: ${errorData.error}` ); } } catch (err) { setError(`Failed to complete authentication: ${err}`); } window.removeEventListener("message", messageHandler); } else if (event.data.type === "oauth-error") { popup?.close(); setError(`OAuth failed: ${event.data.error}`); window.removeEventListener("message", messageHandler); } }; window.addEventListener("message", messageHandler); } else { setError(data.error || "Connection failed"); } } else { setSessionId(data.sessionId); setIsConnected(true); await loadTools(data.sessionId); } } catch (err: unknown) { if (err instanceof Error) { setError(`Connection failed: ${err.message}`); } else { setError(`Connection failed: ${err}`); } } finally { setLoading(false); } }; const loadTools = async (currentSessionId?: string) => { const sid = currentSessionId || sessionId; if (!sid) return; try { const response = await fetch(`/api/mcp/tool/list?sessionId=${sid}`); const data = await response.json(); if (response.ok) { setTools(data.tools || []); } else { setError(`Failed to load tools: ${data.error}`); } } catch (err) { setError(`Failed to load tools: ${err}`); } }; const handleCallTool = async () => { if (!selectedTool) return; setLoading(true); setError(null); setToolResult(null); try { let args = {}; if (toolArgs.trim()) { args = JSON.parse(toolArgs); } const response = await fetch("/api/mcp/tool/call", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ toolName: selectedTool, toolArgs: args, sessionId, }), }); const data = await response.json(); if (response.ok) { setToolResult(data.result); } else { setError(`Tool call failed: ${data.error}`); } } catch (err: unknown) { if (err instanceof Error) { setError(`Tool call failed: ${err.message}`); } else { setError(`Tool call failed: ${err}`); } } finally { setLoading(false); } }; const handleDisconnect = async () => { try { if (sessionId) { await fetch("/api/mcp/auth/disconnect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId }), }); } } catch { // Ignore disconnect errors } setSessionId(null); setIsConnected(false); setTools([]); setSelectedTool(""); setToolResult(null); setJsonError(null); }; return (

MCP OAuth Client

{error && (
{error}
)} {!isConnected ? (
setServerUrl(e.target.value)} placeholder="http://localhost:3000/mcp" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
) : (
✅ Connected to {serverUrl}

Call Tool

{selectedTool && (() => { const tool = tools.find((t) => t.name === selectedTool); return tool?.inputSchema?.properties ? (

Expected Parameters:

{Object.entries(tool.inputSchema.properties).map( ([key, schema]) => (
{key} {tool.inputSchema?.required?.includes(key) && ( * )} ({schema.type || "any"}) {schema.description && (
{schema.description}
)}
) )}
) : null; })()}