Skip to content

MCP tools

Modern MCP 2026-07-28 tool calls can return input_required. Advertise clientCapabilities: ["elicitation": ["form": [:]]] and register onElicitationRequest to answer form requests. SwiftAISDK continues the original operation using a new RPC ID, exact opaque requestState and scoped inputResponses. This is protocol continuation, not a transport failure retry. The client permits eight rounds with sixteen inputs each; the original timeout and abort signal cover the entire exchange, including the form callback. Unsupported sampling, roots or URL input requests fail without continuation.

Shared OAuth stores can implement invalidateCredentials(_:context:) and atomically compare the supplied context.tokens before deleting tokens. This preserves credentials installed by a concurrent successful refresh. Existing one-argument implementations continue to receive unconditional invalidation through the default adapter.

Authorization-server metadata changes now throw MCPOAuthAuthorizationServerMismatchError with code authorization_server_mismatch. Catch this type when presenting a distinct reauthorization message; it does not trigger token invalidation or retry.

Use MCP when tools live outside your process or are shared by multiple clients.

import SwiftAISDK
let mcp = try await MCPClient.connect(
transport: try MCPHTTPTransport(url: "https://mcp.example.com/rpc")
)
let tools = try await mcp.tools()
let provider = try AIProviders.openAI()
let model = try provider.languageModel("gpt-4.1-mini")
let answer = try await model.generateText(
"Search the docs and summarize the answer.",
tools: LanguageToolOptions(Array(tools.values), maxSteps: 4)
)
print(answer.text)
  • Your tools are published by an MCP server.
  • Your app should discover tool schemas at runtime.
  • You want the same tool catalog available to multiple Swift clients.

For local processes, use MCPStdioTransport. For remote servers, use MCPHTTPTransport.

SwiftAISDK tracks @ai-sdk/mcp@2.0.81. The current client absorbs its protocol/HTTP transport and OAuth compatibility hardening behind the same MCPClient.connect workflow. Non-successful POST/SSE responses retain HTTP status, URL, and response-body diagnostics; existing tool-discovery code does not need a migration.

For MCPStdioTransport, explicitly configured environment values win over the small inherited process allowlist. This includes intentionally replacing PATH, HOME, or another inherited key for the child process.

OAuth retries reuse a stored authorization-server choice when discovery is temporarily unavailable, and concurrent requests that observe the same stale token coalesce behind one refresh before they retry.

OAuth providers with dynamically registered client credentials should override isClientInformationDynamicallyRegistered() to return true for those stored credentials. On invalid_client or unauthorized_client, SwiftAISDK discards and re-registers only dynamically registered clients; pre-registered client information is preserved. The MCP protocol-version discovery header is also removed when a redirect crosses origins.

Each discovered definition retains its complete raw annotations value and offers typed MCPToolAnnotations access to title, readOnlyHint, destructiveHint, idempotentHint, and openWorldHint. These fields are untrusted hints for presentation and planning; do not use them as an authorization boundary. Malformed values for a known annotation field make tools/list fail instead of silently changing its meaning.

Servers may return structuredContent without a text content array. The client serializes that value into canonical JSON text for the dynamic tool’s model-facing result while preserving the structured value. OAuth discovery also treats an origin-only issuer with or without its trailing slash as the same issuer; path-bearing issuer URLs remain exact.

An invalid_grant caused by a failed refresh invalidates only that captured refresh generation and retries authorization. An authorization-code invalid_grant propagates without deleting unrelated stored tokens or retrying the same code. Event subscriptions are described in the MCP Events recipe.

Discovery, shell execution, and credential binding

Section titled “Discovery, shell execution, and credential binding”

HTTP protocol discovery waits for the response instead of using the one-second stdio probe. Set initializationOptions: .init(timeoutMilliseconds: ...) on MCPClient.connect to bound the complete operation; cancellation still applies. Custom transports opt in through waitsForProtocolVersionDiscovery.

On macOS and Linux, MCPStdioTransport and MCPStdioConfig accept shell: .disabled (the default), .system (/bin/sh), or .executable("/path/to/shell"). Enabled shells interpret the command and joined arguments as shell syntax, including expansions; supply trusted command text. Direct execution preserves literal argument boundaries.

OAuth clients with an existing secret must configure the credentials’ issuer or supply stored authorization-server information before starting authorization. A matching flow pin cannot override a conflicting credential issuer or token endpoint. An invalid_grant retry retains the validated token binding after credential invalidation. Public clients without custom authentication may still start without a preconfigured binding.

Swift custom implementations of authenticateTokenRequest must additionally return true from usesCustomClientAuthentication and supply an issuer or stored binding. Swift protocols cannot infer whether this optional hook was overridden; the explicit capability flag is required to enforce the pre-redirect check.

Set the issuer from the credential’s original registration or trusted app configuration before calling MCPOAuth.auth. Do not copy an MCP server’s newly advertised authorization server into an existing credential just to satisfy the check. Allowlisting an authorization server also does not establish which server issued that credential.

let credentials = MCPOAuthClientInformation(
clientID: "your-registered-client-id",
clientSecret: "read-from-your-secure-store",
issuer: "https://auth.example.com"
)

Return these credentials from clientInformation(). Alternatively, restore a previously established binding through authorizationServerInformation():

let trustedBinding = MCPOAuthAuthorizationServerInformation(
authorizationServerURL: URL(string: "https://auth.example.com")!,
tokenEndpoint: URL(string: "https://auth.example.com/token")!,
issuer: "https://auth.example.com"
)

Implement saveAuthorizationServerInformation(_:), saveState(_:), storedState(), saveCodeVerifier(_:) and codeVerifier() so the validated binding, state and PKCE verifier survive the authorization round trip. Pass the callback’s code, state and optional iss to MCPOAuth.auth as authorizationCode, callbackState and callbackIssuer. The SDK saves the flow binding after checking the configured credential issuer or trusted stored binding, before redirecting to authorization. It saves tokens after the successful code exchange; persist them in saveTokens(_:) for the same account and client.

If your provider overrides authenticateTokenRequest, also declare usesCustomClientAuthentication as true and configure one of the bindings above. A custom actor provider can use:

nonisolated let usesCustomClientAuthentication = true

On MCPOAuthAuthorizationServerMismatchError, stop authorization and resolve the account’s expected server. Do not replace a trusted binding with the discovered value or retry the same secret against it. A rejected refresh can restart on the same validated server after its token generation is invalidated; a changed server is still rejected. An authorization-code invalid_grant is not retried.

From the repository root, run:

Terminal window
swift run --package-path Examples MCPOAuthMigration

The complete example implements an actor provider and a strict mock transport. It executes both an issuer-bound secret flow and a custom-authentication flow with a stored binding, including the redirect, state/PKCE storage, callback and token exchange. It uses only dummy values, makes no network requests, opens no browser, reads no environment credentials and prints no tokens or secrets. The custom assertion is an inert mock, not an implementation of JWT signing. A successful run ends with MCP_OAUTH_MIGRATION_OFFLINE_OK.

For a real app, use its secure credential store and authorization-session callback, and supply real client authentication from a trusted configuration. The offline example proves the SDK flow and migration wiring; it does not verify a live server’s registration, consent or token policy.