Some checks failed
CI / Test (macos-latest, Swift 6.1.0) (push) Has been cancelled
CI / Test (ubuntu-latest, Swift 6.1.0) (push) Has been cancelled
CI / MCP Conformance Tests (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / Linux Static SDK Build (6.1.2 - ubuntu-latest) (push) Has been cancelled
* feat: added http transport context to the task local execution * fix: added refresh_token to the grantTypes for DCR * chore: updated documentation
1635 lines
51 KiB
Markdown
1635 lines
51 KiB
Markdown
# MCP Swift SDK
|
||
|
||
Official Swift SDK for the [Model Context Protocol][mcp] (MCP).
|
||
|
||
## Overview
|
||
|
||
The Model Context Protocol (MCP) defines a standardized way
|
||
for applications to communicate with AI and ML models.
|
||
This Swift SDK implements both client and server components
|
||
according to the [2025-11-25][mcp-spec-2025-11-25] (latest) version
|
||
of the MCP specification.
|
||
|
||
## Table of contents
|
||
|
||
- [Requirements](#requirements)
|
||
- [Installation](#installation)
|
||
- [Client Usage](#client-usage)
|
||
- [Basic Client Setup](#basic-client-setup)
|
||
- [Transport Options for Clients](#transport-options-for-clients)
|
||
- [Tools](#tools)
|
||
- [Resources](#resources)
|
||
- [Prompts](#prompts)
|
||
- [Completions](#completions)
|
||
- [Sampling](#sampling)
|
||
- [Elicitation](#elicitation)
|
||
- [Roots](#roots)
|
||
- [Logging](#logging)
|
||
- [Error Handling](#error-handling)
|
||
- [Cancellation](#cancellation)
|
||
- [Progress Tracking](#progress-tracking)
|
||
- [Advanced Client Features](#advanced-client-features)
|
||
- [Server Usage](#server-usage)
|
||
- [Basic Server Setup](#basic-server-setup)
|
||
- [Tools](#tools-1)
|
||
- [Resources](#resources-1)
|
||
- [Prompts](#prompts-1)
|
||
- [Completions](#completions-1)
|
||
- [Sampling](#sampling-1)
|
||
- [Elicitations](#elicitations)
|
||
- [Roots](#roots-1)
|
||
- [Logging](#logging-1)
|
||
- [Progress Tracking](#progress-tracking-1)
|
||
- [Initialize Hook](#initialize-hook)
|
||
- [HTTP Request Context in Handlers](#http-request-context-in-handlers)
|
||
- [Graceful Shutdown](#graceful-shutdown)
|
||
- [Transports](#transports)
|
||
- [Authentication](#authentication)
|
||
- [Client: Client Credentials Flow](#client-client-credentials-flow)
|
||
- [Client: Authorization Code Flow](#client-authorization-code-flow)
|
||
- [Client: Custom Token Provider](#client-custom-token-provider)
|
||
- [Client: Custom Token Storage](#client-custom-token-storage)
|
||
- [Client: private\_key\_jwt Authentication](#client-private_key_jwt-authentication)
|
||
- [Client: Endpoint Overrides](#client-endpoint-overrides)
|
||
- [Server: Serving Protected Resource Metadata](#server-serving-protected-resource-metadata)
|
||
- [Server: Validating Bearer Tokens](#server-validating-bearer-tokens)
|
||
- [Platform Availability](#platform-availability)
|
||
- [Debugging and Logging](#debugging-and-logging)
|
||
- [Additional Resources](#additional-resources)
|
||
- [Changelog](#changelog)
|
||
- [License](#license)
|
||
|
||
## Requirements
|
||
|
||
- Swift 6.0+ (Xcode 16+)
|
||
|
||
See the [Platform Availability](#platform-availability) section below
|
||
for platform-specific requirements.
|
||
|
||
## Installation
|
||
|
||
### Swift Package Manager
|
||
|
||
Add the following to your `Package.swift` file:
|
||
|
||
```swift
|
||
dependencies: [
|
||
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.11.0")
|
||
]
|
||
```
|
||
|
||
Then add the dependency to your target:
|
||
|
||
```swift
|
||
.target(
|
||
name: "YourTarget",
|
||
dependencies: [
|
||
.product(name: "MCP", package: "swift-sdk")
|
||
]
|
||
)
|
||
```
|
||
|
||
## Client Usage
|
||
|
||
The client component allows your application to connect to MCP servers.
|
||
|
||
### Basic Client Setup
|
||
|
||
```swift
|
||
import MCP
|
||
|
||
// Initialize the client
|
||
let client = Client(name: "MyApp", version: "1.0.0")
|
||
|
||
// Create a transport and connect
|
||
let transport = StdioTransport()
|
||
let result = try await client.connect(transport: transport)
|
||
|
||
// Check server capabilities
|
||
if result.capabilities.tools != nil {
|
||
// Server supports tools (implicitly including tool calling if the 'tools' capability object is present)
|
||
}
|
||
```
|
||
|
||
> [!NOTE]
|
||
> The `Client.connect(transport:)` method returns the initialization result.
|
||
> This return value is discardable,
|
||
> so you can ignore it if you don't need to check server capabilities.
|
||
|
||
### Transport Options for Clients
|
||
|
||
#### Stdio Transport
|
||
|
||
For local subprocess communication:
|
||
|
||
```swift
|
||
// Create a stdio transport (simplest option)
|
||
let transport = StdioTransport()
|
||
try await client.connect(transport: transport)
|
||
```
|
||
|
||
#### HTTP Transport
|
||
|
||
For remote server communication:
|
||
|
||
```swift
|
||
// Create a streaming HTTP transport
|
||
let transport = HTTPClientTransport(
|
||
endpoint: URL(string: "http://localhost:8080")!,
|
||
streaming: true // Enable Server-Sent Events for real-time updates
|
||
)
|
||
try await client.connect(transport: transport)
|
||
```
|
||
|
||
### Tools
|
||
|
||
Tools represent functions that can be called by the client:
|
||
|
||
```swift
|
||
// List available tools
|
||
let (tools, cursor) = try await client.listTools()
|
||
print("Available tools: \(tools.map { $0.name }.joined(separator: ", "))")
|
||
|
||
// Call a tool with arguments and get the result
|
||
let (content, isError) = try await client.callTool(
|
||
name: "image-generator",
|
||
arguments: [
|
||
"prompt": "A serene mountain landscape at sunset",
|
||
"style": "photorealistic",
|
||
"width": 1024,
|
||
"height": 768
|
||
]
|
||
)
|
||
|
||
// Handle tool content
|
||
for item in content {
|
||
switch item {
|
||
case .text(let text):
|
||
print("Generated text: \(text)")
|
||
case .image(let data, let mimeType, let metadata):
|
||
if let width = metadata?["width"] as? Int,
|
||
let height = metadata?["height"] as? Int {
|
||
print("Generated \(width)x\(height) image of type \(mimeType)")
|
||
// Save or display the image data
|
||
}
|
||
case .audio(let data, let mimeType):
|
||
print("Received audio data of type \(mimeType)")
|
||
case .resource(let resource, _, _):
|
||
print("Received embedded resource: \(resource)")
|
||
case .resourceLink(let uri, let name, _, _, let mimeType, _):
|
||
print("Resource link: \(name) at \(uri), type: \(mimeType ?? "unknown")")
|
||
}
|
||
}
|
||
```
|
||
|
||
### Resources
|
||
|
||
Resources represent data that can be accessed and potentially subscribed to:
|
||
|
||
```swift
|
||
// List available resources
|
||
let (resources, nextCursor) = try await client.listResources()
|
||
print("Available resources: \(resources.map { $0.uri }.joined(separator: ", "))")
|
||
|
||
// Read a resource
|
||
let contents = try await client.readResource(uri: "resource://example")
|
||
print("Resource content: \(contents)")
|
||
|
||
// Subscribe to resource updates if supported
|
||
if result.capabilities.resources?.subscribe == true {
|
||
try await client.subscribeToResource(uri: "resource://example")
|
||
|
||
// Register notification handler
|
||
await client.onNotification(ResourceUpdatedNotification.self) { message in
|
||
let uri = message.params.uri
|
||
print("Resource \(uri) updated with new content")
|
||
|
||
// Fetch the updated resource content
|
||
let updatedContents = try await client.readResource(uri: uri)
|
||
print("Updated resource content received")
|
||
}
|
||
}
|
||
```
|
||
|
||
### Prompts
|
||
|
||
Prompts represent templated conversation starters:
|
||
|
||
```swift
|
||
// List available prompts
|
||
let (prompts, nextCursor) = try await client.listPrompts()
|
||
print("Available prompts: \(prompts.map { $0.name }.joined(separator: ", "))")
|
||
|
||
// Get a prompt with arguments
|
||
let (description, messages) = try await client.getPrompt(
|
||
name: "customer-service",
|
||
arguments: [
|
||
"customerName": "Alice",
|
||
"orderNumber": "ORD-12345",
|
||
"issue": "delivery delay"
|
||
]
|
||
)
|
||
|
||
// Use the prompt messages in your application
|
||
print("Prompt description: \(description)")
|
||
for message in messages {
|
||
if case .text(text: let text) = message.content {
|
||
print("\(message.role): \(text)")
|
||
}
|
||
}
|
||
```
|
||
|
||
### Completions
|
||
|
||
Completions allow servers to provide autocompletion suggestions for prompt and resource template arguments as users type:
|
||
|
||
```swift
|
||
// Request completions for a prompt argument
|
||
let completion = try await client.complete(
|
||
promptName: "code_review",
|
||
argumentName: "language",
|
||
argumentValue: "py"
|
||
)
|
||
|
||
// Display suggestions to the user
|
||
for value in completion.values {
|
||
print("Suggestion: \(value)")
|
||
}
|
||
|
||
if completion.hasMore == true {
|
||
print("More suggestions available (total: \(completion.total ?? 0))")
|
||
}
|
||
```
|
||
|
||
You can also provide context with already-resolved arguments:
|
||
|
||
```swift
|
||
// First, user selects a language
|
||
let languageCompletion = try await client.complete(
|
||
promptName: "code_review",
|
||
argumentName: "language",
|
||
argumentValue: "py"
|
||
)
|
||
// User selects "python"
|
||
|
||
// Then get framework suggestions based on the selected language
|
||
let frameworkCompletion = try await client.complete(
|
||
promptName: "code_review",
|
||
argumentName: "framework",
|
||
argumentValue: "fla",
|
||
context: ["language": .string("python")]
|
||
)
|
||
// Returns: ["flask"]
|
||
```
|
||
|
||
Completions work for resource templates as well:
|
||
|
||
```swift
|
||
// Get path completions for a resource URI template
|
||
let pathCompletion = try await client.complete(
|
||
resourceURI: "file:///{path}",
|
||
argumentName: "path",
|
||
argumentValue: "/usr/"
|
||
)
|
||
// Returns: ["/usr/bin", "/usr/lib", "/usr/local"]
|
||
```
|
||
|
||
### Sampling
|
||
|
||
Sampling allows servers to request LLM completions through the client,
|
||
enabling agentic behaviors while maintaining human-in-the-loop control.
|
||
Clients register a handler to process incoming sampling requests from servers.
|
||
|
||
> [!TIP]
|
||
> Sampling requests flow from **server to client**,
|
||
> not client to server.
|
||
> This enables servers to request AI assistance
|
||
> while clients maintain control over model access and user approval.
|
||
|
||
```swift
|
||
// Register a sampling handler in the client
|
||
await client.withSamplingHandler { parameters in
|
||
// Review the sampling request (human-in-the-loop step 1)
|
||
print("Server requests completion for: \(parameters.messages)")
|
||
|
||
// Optionally modify the request based on user input
|
||
var messages = parameters.messages
|
||
if let systemPrompt = parameters.systemPrompt {
|
||
print("System prompt: \(systemPrompt)")
|
||
}
|
||
|
||
// Sample from your LLM (this is where you'd call your AI service)
|
||
let completion = try await callYourLLMService(
|
||
messages: messages,
|
||
maxTokens: parameters.maxTokens,
|
||
temperature: parameters.temperature
|
||
)
|
||
|
||
// Review the completion (human-in-the-loop step 2)
|
||
print("LLM generated: \(completion)")
|
||
// User can approve, modify, or reject the completion here
|
||
|
||
// Return the result to the server
|
||
return CreateSamplingMessage.Result(
|
||
model: "your-model-name",
|
||
stopReason: .endTurn,
|
||
role: .assistant,
|
||
content: .text(completion)
|
||
)
|
||
}
|
||
```
|
||
|
||
### Elicitation
|
||
|
||
Elicitation allows servers to request structured information directly from users through the client.
|
||
This is useful when servers need user input that wasn't provided in the original request,
|
||
such as credentials, configuration choices, or approval for sensitive operations.
|
||
|
||
> [!TIP]
|
||
> Elicitation requests flow from **server to client**,
|
||
> similar to sampling.
|
||
> Clients must register a handler to respond to elicitation requests from servers.
|
||
|
||
#### Client-Side: Handling Elicitation Requests
|
||
|
||
Register an elicitation handler to respond to server requests:
|
||
|
||
```swift
|
||
// Register an elicitation handler in the client
|
||
await client.withElicitationHandler { parameters in
|
||
switch parameters {
|
||
case .form(let form):
|
||
// Display the request to the user
|
||
print("Server requests: \(form.message)")
|
||
|
||
// If a schema was provided, inspect it
|
||
if let schema = form.requestedSchema {
|
||
print("Required fields: \(schema.required ?? [])")
|
||
print("Schema: \(schema.properties)")
|
||
}
|
||
|
||
// Present UI to collect user input
|
||
let userResponse = presentElicitationUI(form)
|
||
|
||
// Return the user's response
|
||
if userResponse.accepted {
|
||
return CreateElicitation.Result(
|
||
action: .accept,
|
||
content: userResponse.data
|
||
)
|
||
} else if userResponse.canceled {
|
||
return CreateElicitation.Result(action: .cancel)
|
||
} else {
|
||
return CreateElicitation.Result(action: .decline)
|
||
}
|
||
|
||
case .url(let url):
|
||
// Direct the user to an external URL (e.g., for OAuth)
|
||
openURL(url.url)
|
||
return CreateElicitation.Result(action: .accept)
|
||
}
|
||
}
|
||
```
|
||
|
||
Common use cases for elicitation:
|
||
|
||
- **Authentication**: Request credentials when needed rather than upfront
|
||
- **Confirmation**: Ask for user approval before sensitive operations
|
||
- **Configuration**: Collect preferences or settings during operation
|
||
- **Missing information**: Request additional details not provided initially
|
||
|
||
### Roots
|
||
|
||
Roots define the filesystem boundaries that a client exposes to servers. Servers discover roots by sending a `roots/list` request to the client; clients notify servers when the list changes.
|
||
|
||
> [!TIP]
|
||
> To use roots, declare the `roots` capability when creating the client.
|
||
|
||
```swift
|
||
let client = Client(
|
||
name: "MyApp",
|
||
version: "1.0.0",
|
||
capabilities: .init(
|
||
roots: .init(listChanged: true)
|
||
)
|
||
)
|
||
|
||
// Register a handler for roots/list requests from servers
|
||
await client.withRootsHandler {
|
||
return [
|
||
Root(uri: "file:///Users/user/projects", name: "Projects"),
|
||
Root(uri: "file:///Users/user/documents", name: "Documents")
|
||
]
|
||
}
|
||
|
||
// Notify connected servers whenever roots change
|
||
try await client.notifyRootsChanged()
|
||
```
|
||
|
||
### Logging
|
||
|
||
Clients can control server logging levels and receive structured log messages:
|
||
|
||
```swift
|
||
// Set the minimum logging level
|
||
try await client.setLoggingLevel(.warning)
|
||
|
||
// Register a handler for log messages from the server
|
||
await client.onNotification(LogMessageNotification.self) { message in
|
||
let level = message.params.level // LogLevel (debug, info, warning, etc.)
|
||
let logger = message.params.logger // Optional logger name
|
||
let data = message.params.data // Arbitrary JSON data
|
||
|
||
// Display log message based on level
|
||
switch level {
|
||
case .error, .critical, .alert, .emergency:
|
||
print("❌ [\(logger ?? "server")] \(data)")
|
||
case .warning:
|
||
print("⚠️ [\(logger ?? "server")] \(data)")
|
||
default:
|
||
print("ℹ️ [\(logger ?? "server")] \(data)")
|
||
}
|
||
}
|
||
```
|
||
|
||
Log levels follow the standard syslog severity levels (RFC 5424):
|
||
|
||
- **debug**: Detailed debugging information
|
||
- **info**: General informational messages
|
||
- **notice**: Normal but significant events
|
||
- **warning**: Warning conditions
|
||
- **error**: Error conditions
|
||
- **critical**: Critical conditions
|
||
- **alert**: Action must be taken immediately
|
||
- **emergency**: System is unusable
|
||
|
||
### Error Handling
|
||
|
||
Handle common client errors:
|
||
|
||
```swift
|
||
do {
|
||
try await client.connect(transport: transport)
|
||
// Success
|
||
} catch let error as MCPError {
|
||
print("MCP Error: \(error.localizedDescription)")
|
||
} catch {
|
||
print("Unexpected error: \(error)")
|
||
}
|
||
```
|
||
|
||
### Cancellation
|
||
|
||
Either side can cancel an in-progress request and handle incoming cancellations gracefully:
|
||
|
||
#### Option 1: Convenience Methods with RequestContext Overload
|
||
|
||
For common operations like tool calls, use the overloaded method that returns `RequestContext`:
|
||
|
||
```swift
|
||
// Call a tool and get a context for cancellation
|
||
let context = try client.callTool(
|
||
name: "long-running-analysis",
|
||
arguments: ["data": largeDataset]
|
||
)
|
||
|
||
// You can cancel the request at any time
|
||
try await client.cancelRequest(context.requestID, reason: "User cancelled")
|
||
|
||
// Await the result (will throw CancellationError if cancelled)
|
||
do {
|
||
let result = try await context.value
|
||
print("Result: \(result.content)")
|
||
} catch is CancellationError {
|
||
print("Request was cancelled")
|
||
}
|
||
```
|
||
|
||
#### Option 2: Direct send() for Maximum Flexibility
|
||
|
||
For full control or custom requests, use `send()` directly:
|
||
|
||
```swift
|
||
// Create any request type
|
||
let request = CallTool.request(.init(
|
||
name: "long-running-analysis",
|
||
arguments: ["data": largeDataset]
|
||
))
|
||
|
||
// Send and get a context for cancellation tracking
|
||
let context: RequestContext<CallTool.Result> = try client.send(request)
|
||
|
||
// Cancel when needed
|
||
try await client.cancelRequest(context.requestID, reason: "Timeout")
|
||
|
||
// Get the result
|
||
let result = try await context.value
|
||
let content = result.content
|
||
let isError = result.isError
|
||
```
|
||
|
||
### Progress tracking
|
||
|
||
Clients can attach a progress token to a request and receive incremental progress updates for long-running operations:
|
||
|
||
```swift
|
||
// Call a tool with progress tracking
|
||
let progressToken = ProgressToken.unique()
|
||
|
||
// Register a notification handler to receive progress updates
|
||
await client.onNotification(ProgressNotification.self) { message in
|
||
let params = message.params
|
||
// Filter by your progress token
|
||
if params.progressToken == progressToken {
|
||
print("Progress: \(params.progress)/\(params.total ?? 0)")
|
||
if let message = params.message {
|
||
print("Status: \(message)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Make the request with the progress token
|
||
let (content, isError) = try await client.callTool(
|
||
name: "long-running-tool",
|
||
arguments: ["input": "value"],
|
||
meta: Metadata(progressToken: progressToken)
|
||
)
|
||
```
|
||
|
||
### Advanced Client Features
|
||
|
||
#### Strict vs Non-Strict Configuration
|
||
|
||
Configure client behavior for capability checking:
|
||
|
||
```swift
|
||
// Strict configuration - fail fast if a capability is missing
|
||
let strictClient = Client(
|
||
name: "StrictClient",
|
||
version: "1.0.0",
|
||
configuration: .strict
|
||
)
|
||
|
||
// With strict configuration, calling a method for an unsupported capability
|
||
// will throw an error immediately without sending a request
|
||
do {
|
||
// This will throw an error if resources.list capability is not available
|
||
let resources = try await strictClient.listResources()
|
||
} catch let error as MCPError {
|
||
print("Capability not available: \(error.localizedDescription)")
|
||
}
|
||
|
||
// Default (non-strict) configuration - attempt the request anyway
|
||
let client = Client(
|
||
name: "FlexibleClient",
|
||
version: "1.0.0",
|
||
configuration: .default
|
||
)
|
||
|
||
// With default configuration, the client will attempt the request
|
||
// even if the capability wasn't advertised by the server
|
||
do {
|
||
let resources = try await client.listResources()
|
||
} catch let error as MCPError {
|
||
// Still handle the error if the server rejects the request
|
||
print("Server rejected request: \(error.localizedDescription)")
|
||
}
|
||
```
|
||
|
||
#### Request Batching
|
||
|
||
Improve performance by sending multiple requests in a single batch:
|
||
|
||
```swift
|
||
// Array to hold tool call tasks
|
||
var toolTasks: [Task<CallTool.Result, Swift.Error>] = []
|
||
|
||
// Send a batch of requests
|
||
try await client.withBatch { batch in
|
||
// Add multiple tool calls to the batch
|
||
for i in 0..<10 {
|
||
toolTasks.append(
|
||
try await batch.addRequest(
|
||
CallTool.request(.init(name: "square", arguments: ["n": Value(i)]))
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
// Process results after the batch is sent
|
||
print("Processing \(toolTasks.count) tool results...")
|
||
for (index, task) in toolTasks.enumerated() {
|
||
do {
|
||
let result = try await task.value
|
||
print("\(index): \(result.content)")
|
||
} catch {
|
||
print("\(index) failed: \(error)")
|
||
}
|
||
}
|
||
```
|
||
|
||
You can also batch different types of requests:
|
||
|
||
```swift
|
||
// Declare task variables
|
||
var pingTask: Task<Ping.Result, Error>?
|
||
var promptTask: Task<GetPrompt.Result, Error>?
|
||
|
||
// Send a batch with different request types
|
||
try await client.withBatch { batch in
|
||
pingTask = try await batch.addRequest(Ping.request())
|
||
promptTask = try await batch.addRequest(
|
||
GetPrompt.request(.init(name: "greeting"))
|
||
)
|
||
}
|
||
|
||
// Process individual results
|
||
do {
|
||
if let pingTask = pingTask {
|
||
try await pingTask.value
|
||
print("Ping successful")
|
||
}
|
||
|
||
if let promptTask = promptTask {
|
||
let promptResult = try await promptTask.value
|
||
print("Prompt: \(promptResult.description ?? "None")")
|
||
}
|
||
} catch {
|
||
print("Error processing batch results: \(error)")
|
||
}
|
||
```
|
||
|
||
> [!NOTE]
|
||
> `Server` automatically handles batch requests from MCP clients.
|
||
|
||
## Server Usage
|
||
|
||
The server component allows your application to host model capabilities and respond to client requests.
|
||
|
||
### Basic Server Setup
|
||
|
||
```swift
|
||
import MCP
|
||
|
||
// Create a server with given capabilities
|
||
let server = Server(
|
||
name: "MyModelServer",
|
||
version: "1.0.0",
|
||
capabilities: .init(
|
||
completions: .init(),
|
||
logging: .init(),
|
||
prompts: .init(listChanged: true),
|
||
resources: .init(subscribe: true, listChanged: true),
|
||
tools: .init(listChanged: true)
|
||
)
|
||
)
|
||
|
||
// Create transport and start server
|
||
let transport = StdioTransport()
|
||
try await server.start(transport: transport)
|
||
|
||
// Now register handlers for the capabilities you've enabled
|
||
```
|
||
|
||
### Tools
|
||
|
||
Register tool handlers to respond to client tool calls:
|
||
|
||
```swift
|
||
// Register a tool list handler
|
||
await server.withMethodHandler(ListTools.self) { _ in
|
||
let tools = [
|
||
Tool(
|
||
name: "weather",
|
||
description: "Get current weather for a location",
|
||
inputSchema: .object([
|
||
"properties": .object([
|
||
"location": .string("City name or coordinates"),
|
||
"units": .string("Units of measurement, e.g., metric, imperial")
|
||
])
|
||
])
|
||
),
|
||
Tool(
|
||
name: "calculator",
|
||
description: "Perform calculations",
|
||
inputSchema: .object([
|
||
"properties": .object([
|
||
"expression": .string("Mathematical expression to evaluate")
|
||
])
|
||
])
|
||
)
|
||
]
|
||
return .init(tools: tools)
|
||
}
|
||
|
||
// Register a tool call handler
|
||
await server.withMethodHandler(CallTool.self) { params in
|
||
switch params.name {
|
||
case "weather":
|
||
let location = params.arguments?["location"]?.stringValue ?? "Unknown"
|
||
let units = params.arguments?["units"]?.stringValue ?? "metric"
|
||
let weatherData = getWeatherData(location: location, units: units) // Your implementation
|
||
return .init(
|
||
content: [.text("Weather for \(location): \(weatherData.temperature)°, \(weatherData.conditions)")],
|
||
isError: false
|
||
)
|
||
|
||
case "calculator":
|
||
if let expression = params.arguments?["expression"]?.stringValue {
|
||
let result = evaluateExpression(expression) // Your implementation
|
||
return .init(content: [.text("\(result)")], isError: false)
|
||
} else {
|
||
return .init(content: [.text("Missing expression parameter")], isError: true)
|
||
}
|
||
|
||
default:
|
||
return .init(content: [.text("Unknown tool")], isError: true)
|
||
}
|
||
}
|
||
```
|
||
|
||
### Resources
|
||
|
||
Implement resource handlers for data access:
|
||
|
||
```swift
|
||
// Register a resource list handler
|
||
await server.withMethodHandler(ListResources.self) { params in
|
||
let resources = [
|
||
Resource(
|
||
name: "Knowledge Base Articles",
|
||
uri: "resource://knowledge-base/articles",
|
||
description: "Collection of support articles and documentation"
|
||
),
|
||
Resource(
|
||
name: "System Status",
|
||
uri: "resource://system/status",
|
||
description: "Current system operational status"
|
||
)
|
||
]
|
||
return .init(resources: resources, nextCursor: nil)
|
||
}
|
||
|
||
// Register a resource read handler
|
||
await server.withMethodHandler(ReadResource.self) { params in
|
||
switch params.uri {
|
||
case "resource://knowledge-base/articles":
|
||
return .init(contents: [Resource.Content.text("# Knowledge Base\n\nThis is the content of the knowledge base...", uri: params.uri)])
|
||
|
||
case "resource://system/status":
|
||
let status = getCurrentSystemStatus() // Your implementation
|
||
let statusJson = """
|
||
{
|
||
"status": "\(status.overall)",
|
||
"components": {
|
||
"database": "\(status.database)",
|
||
"api": "\(status.api)",
|
||
"model": "\(status.model)"
|
||
},
|
||
"lastUpdated": "\(status.timestamp)"
|
||
}
|
||
"""
|
||
return .init(contents: [Resource.Content.text(statusJson, uri: params.uri, mimeType: "application/json")])
|
||
|
||
default:
|
||
throw MCPError.invalidParams("Unknown resource URI: \(params.uri)")
|
||
}
|
||
}
|
||
|
||
// Register a resource subscribe handler
|
||
await server.withMethodHandler(ResourceSubscribe.self) { params in
|
||
// Store subscription for later notifications.
|
||
// Client identity for multi-client scenarios needs to be managed by the server application,
|
||
// potentially using information from the initialize handshake if the server handles one client post-init.
|
||
// addSubscription(clientID: /* some_client_identifier */, uri: params.uri)
|
||
print("Client subscribed to \(params.uri). Server needs to implement logic to track this subscription.")
|
||
return .init()
|
||
}
|
||
```
|
||
|
||
### Prompts
|
||
|
||
Implement prompt handlers:
|
||
|
||
```swift
|
||
// Register a prompt list handler
|
||
await server.withMethodHandler(ListPrompts.self) { params in
|
||
let prompts = [
|
||
Prompt(
|
||
name: "interview",
|
||
description: "Job interview conversation starter",
|
||
arguments: [
|
||
.init(name: "position", description: "Job position", required: true),
|
||
.init(name: "company", description: "Company name", required: true),
|
||
.init(name: "interviewee", description: "Candidate name")
|
||
]
|
||
),
|
||
Prompt(
|
||
name: "customer-support",
|
||
description: "Customer support conversation starter",
|
||
arguments: [
|
||
.init(name: "issue", description: "Customer issue", required: true),
|
||
.init(name: "product", description: "Product name", required: true)
|
||
]
|
||
)
|
||
]
|
||
return .init(prompts: prompts, nextCursor: nil)
|
||
}
|
||
|
||
// Register a prompt get handler
|
||
await server.withMethodHandler(GetPrompt.self) { params in
|
||
switch params.name {
|
||
case "interview":
|
||
let position = params.arguments?["position"]?.stringValue ?? "Software Engineer"
|
||
let company = params.arguments?["company"]?.stringValue ?? "Acme Corp"
|
||
let interviewee = params.arguments?["interviewee"]?.stringValue ?? "Candidate"
|
||
|
||
let description = "Job interview for \(position) position at \(company)"
|
||
let messages: [Prompt.Message] = [
|
||
.user(.text(text: "You are an interviewer for the \(position) position at \(company).")),
|
||
.user(.text(text: "Hello, I'm \(interviewee) and I'm here for the \(position) interview.")),
|
||
.assistant(.text(text: "Hi \(interviewee), welcome to \(company)! I'd like to start by asking about your background and experience."))
|
||
]
|
||
|
||
return .init(description: description, messages: messages)
|
||
|
||
case "customer-support":
|
||
// Similar implementation for customer support prompt
|
||
|
||
default:
|
||
throw MCPError.invalidParams("Unknown prompt name: \(params.name)")
|
||
}
|
||
}
|
||
```
|
||
|
||
### Completions
|
||
|
||
Servers can provide autocompletion suggestions for prompt and resource template arguments:
|
||
|
||
```swift
|
||
// Enable completions capability
|
||
let server = Server(
|
||
name: "MyServer",
|
||
version: "1.0.0",
|
||
capabilities: .init(
|
||
completions: .init(),
|
||
prompts: .init(listChanged: true)
|
||
)
|
||
)
|
||
|
||
// Register a completion handler
|
||
await server.withMethodHandler(Complete.self) { params in
|
||
// Get the argument being completed
|
||
let argumentName = params.argument.name
|
||
let currentValue = params.argument.value
|
||
|
||
// Check which prompt or resource is being completed
|
||
switch params.ref {
|
||
case .prompt(let promptRef):
|
||
// Provide completions for a prompt argument
|
||
if promptRef.name == "code_review" && argumentName == "language" {
|
||
// Simple prefix matching
|
||
let allLanguages = ["python", "perl", "php", "javascript", "java", "swift"]
|
||
let matches = allLanguages.filter { $0.hasPrefix(currentValue.lowercased()) }
|
||
|
||
return .init(
|
||
completion: .init(
|
||
values: Array(matches.prefix(100)), // Max 100 items
|
||
total: matches.count,
|
||
hasMore: matches.count > 100
|
||
)
|
||
)
|
||
}
|
||
|
||
case .resource(let resourceRef):
|
||
// Provide completions for a resource template argument
|
||
if resourceRef.uri == "file:///{path}" && argumentName == "path" {
|
||
// Return directory suggestions
|
||
let suggestions = try getDirectoryCompletions(for: currentValue)
|
||
return .init(
|
||
completion: .init(
|
||
values: suggestions,
|
||
total: suggestions.count,
|
||
hasMore: false
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
// No completions available
|
||
return .init(completion: .init(values: [], total: 0, hasMore: false))
|
||
}
|
||
```
|
||
|
||
You can also use context from already-resolved arguments:
|
||
|
||
```swift
|
||
await server.withMethodHandler(Complete.self) { params in
|
||
// Access context from previous argument completions
|
||
if let context = params.context,
|
||
let language = context.arguments["language"]?.stringValue {
|
||
|
||
// Provide framework suggestions based on selected language
|
||
if language == "python" {
|
||
let frameworks = ["flask", "django", "fastapi", "tornado"]
|
||
let matches = frameworks.filter {
|
||
$0.hasPrefix(params.argument.value.lowercased())
|
||
}
|
||
return .init(
|
||
completion: .init(values: matches, total: matches.count, hasMore: false)
|
||
)
|
||
}
|
||
}
|
||
|
||
return .init(completion: .init(values: [], total: 0, hasMore: false))
|
||
}
|
||
```
|
||
|
||
### Sampling
|
||
|
||
Servers can request LLM completions from clients through sampling. This enables agentic behaviors where servers can ask for AI assistance while maintaining human oversight.
|
||
|
||
```swift
|
||
// Enable sampling capability in server
|
||
let server = Server(
|
||
name: "MyModelServer",
|
||
version: "1.0.0",
|
||
capabilities: .init(
|
||
sampling: .init(), // Enable sampling capability
|
||
tools: .init(listChanged: true)
|
||
)
|
||
)
|
||
|
||
// Request sampling from the connected client
|
||
do {
|
||
let result = try await server.requestSampling(
|
||
messages: [
|
||
.user("Analyze this data and suggest next steps")
|
||
],
|
||
systemPrompt: "You are a helpful data analyst",
|
||
temperature: 0.7,
|
||
maxTokens: 150
|
||
)
|
||
|
||
// Use the LLM completion in your server logic
|
||
print("LLM suggested: \(result.content)")
|
||
|
||
} catch {
|
||
print("Sampling request failed: \(error)")
|
||
}
|
||
```
|
||
|
||
Sampling enables powerful agentic workflows:
|
||
|
||
- **Decision-making**: Ask the LLM to choose between options
|
||
- **Content generation**: Request drafts for user approval
|
||
- **Data analysis**: Get AI insights on complex data
|
||
- **Multi-step reasoning**: Chain AI completions with tool calls
|
||
|
||
### Elicitations
|
||
|
||
Servers can request information from users through elicitation:
|
||
|
||
```swift
|
||
// Ask the user to provide some additional information
|
||
let schema = Elicitation.RequestSchema(
|
||
title: "Additional Information Required",
|
||
description: "Please provide the following details to continue",
|
||
properties: [
|
||
"name": .object([
|
||
"type": .string("string"),
|
||
"description": .string("Your full name")
|
||
]),
|
||
"confirmed": .object([
|
||
"type": .string("boolean"),
|
||
"description": .string("Do you confirm the provided information?")
|
||
])
|
||
],
|
||
required: ["name", "confirmed"]
|
||
)
|
||
|
||
let result = try await server.requestElicitation(
|
||
message: "Some details are needed before proceeding",
|
||
requestedSchema: schema
|
||
)
|
||
|
||
switch result.action {
|
||
case .accept:
|
||
if let content = result.content {
|
||
let name = content["name"]?.stringValue
|
||
let confirmed = content["confirmed"]?.boolValue
|
||
// Use the collected data...
|
||
}
|
||
case .decline:
|
||
throw MCPError.invalidRequest("User declined to provide information")
|
||
case .cancel:
|
||
throw MCPError.invalidRequest("Operation canceled by user")
|
||
}
|
||
```
|
||
|
||
For URL-based elicitation (e.g., OAuth flows), use the URL overload:
|
||
|
||
```swift
|
||
let result = try await server.requestElicitation(
|
||
message: "Please sign in to continue",
|
||
url: "https://example.com/oauth/authorize?client_id=...",
|
||
elicitationId: UUID().uuidString
|
||
)
|
||
```
|
||
|
||
### Roots
|
||
|
||
Servers can request the list of filesystem roots that the client has exposed:
|
||
|
||
```swift
|
||
// Request roots from the connected client
|
||
// (requires the client to declare the roots capability)
|
||
let roots = try await server.listRoots()
|
||
for root in roots {
|
||
print("Root: \(root.name ?? root.uri) at \(root.uri)")
|
||
}
|
||
|
||
// React to root list changes
|
||
await server.onNotification(RootsListChangedNotification.self) { _ in
|
||
let updatedRoots = try await server.listRoots()
|
||
print("Roots updated: \(updatedRoots.map { $0.uri })")
|
||
}
|
||
```
|
||
|
||
### Logging
|
||
|
||
Servers can send structured log messages to clients:
|
||
|
||
```swift
|
||
// Enable logging capability
|
||
let server = Server(
|
||
name: "MyServer",
|
||
version: "1.0.0",
|
||
capabilities: .init(
|
||
logging: .init(),
|
||
tools: .init(listChanged: true)
|
||
)
|
||
)
|
||
|
||
// Send log messages at different severity levels
|
||
try await server.log(
|
||
level: .info,
|
||
logger: "database",
|
||
data: Value.object([
|
||
"message": .string("Database connected successfully"),
|
||
"host": .string("localhost"),
|
||
"port": .int(5432)
|
||
])
|
||
)
|
||
|
||
try await server.log(
|
||
level: .error,
|
||
logger: "api",
|
||
data: Value.object([
|
||
"message": .string("Request failed"),
|
||
"statusCode": .int(500),
|
||
"error": .string("Internal server error")
|
||
])
|
||
)
|
||
|
||
// You can also use codable types directly
|
||
struct ErrorLog: Codable {
|
||
let message: String
|
||
let code: Int
|
||
let timestamp: String
|
||
}
|
||
|
||
let errorLog = ErrorLog(
|
||
message: "Operation failed",
|
||
code: 500,
|
||
timestamp: ISO8601DateFormatter().string(from: Date())
|
||
)
|
||
|
||
try await server.log(level: .error, logger: "operations", data: errorLog)
|
||
```
|
||
|
||
Clients can control which log levels they receive:
|
||
|
||
```swift
|
||
// Register a handler for client's logging level preferences
|
||
await server.withMethodHandler(SetLoggingLevel.self) { params in
|
||
let minimumLevel = params.level
|
||
|
||
// Store the client's preference and filter log messages accordingly
|
||
// (Implementation depends on your server architecture)
|
||
storeLogLevel(minimumLevel)
|
||
|
||
return Empty()
|
||
}
|
||
```
|
||
|
||
### Progress Tracking
|
||
|
||
Servers can send incremental progress notifications during long-running tool calls by reading the `progressToken` from the request metadata and sending `ProgressNotification` messages:
|
||
|
||
```swift
|
||
await server.withMethodHandler(CallTool.self) { params in
|
||
// Read the progress token from request metadata
|
||
guard let token = params._meta?.progressToken else {
|
||
// No progress token provided — run without reporting progress
|
||
return .init(content: [.text("Done")], isError: false)
|
||
}
|
||
|
||
// Report initial progress
|
||
let started = ProgressNotification.message(
|
||
.init(progressToken: token, progress: 0, total: 100)
|
||
)
|
||
try await server.notify(started)
|
||
|
||
// ... do work ...
|
||
|
||
// Report intermediate progress
|
||
let halfway = ProgressNotification.message(
|
||
.init(progressToken: token, progress: 50, total: 100, message: "Halfway there")
|
||
)
|
||
try await server.notify(halfway)
|
||
|
||
// ... do more work ...
|
||
|
||
// Report completion
|
||
let done = ProgressNotification.message(
|
||
.init(progressToken: token, progress: 100, total: 100, message: "Complete")
|
||
)
|
||
try await server.notify(done)
|
||
|
||
return .init(content: [.text("Done")], isError: false)
|
||
}
|
||
```
|
||
|
||
#### Initialize Hook
|
||
|
||
Control client connections with an initialize hook:
|
||
|
||
```swift
|
||
// Start the server with an initialize hook
|
||
try await server.start(transport: transport) { clientInfo, clientCapabilities in
|
||
// Validate client info
|
||
guard clientInfo.name != "BlockedClient" else {
|
||
throw MCPError.invalidRequest("This client is not allowed")
|
||
}
|
||
|
||
// You can also inspect client capabilities
|
||
if clientCapabilities.sampling == nil {
|
||
print("Client does not support sampling")
|
||
}
|
||
|
||
// Perform any server-side setup based on client info
|
||
print("Client \(clientInfo.name) v\(clientInfo.version) connected")
|
||
|
||
// If the hook completes without throwing, initialization succeeds
|
||
}
|
||
```
|
||
|
||
### HTTP Request Context in Handlers
|
||
|
||
When a server is connected over `StatefulHTTPServerTransport` or `StatelessHTTPServerTransport`,
|
||
method handlers can observe the originating HTTP request (headers, body, path, method) via
|
||
`Server.currentHandlerContext` — a task-local set automatically before each handler runs:
|
||
|
||
```swift
|
||
await server.withMethodHandler(CallTool.self) { params in
|
||
let httpRequest = Server.currentHandlerContext?.httpContext
|
||
let authHeader = httpRequest?.headers["Authorization"]
|
||
// …use the header to scope the response, audit, etc.
|
||
return .init(content: [.text("ok")], isError: false)
|
||
}
|
||
```
|
||
|
||
`httpContext` is `nil` for transports that don't carry HTTP context (e.g. `StdioTransport`,
|
||
`InMemoryTransport`) and for handlers reached off the dispatch path.
|
||
|
||
Task-locals are not inherited by `Task.detached`. If you spawn a detached task from a handler,
|
||
capture the context up front:
|
||
|
||
```swift
|
||
let ctx = Server.currentHandlerContext
|
||
Task.detached { await doWork(with: ctx?.httpContext) }
|
||
```
|
||
|
||
Custom HTTP transports can opt in by conforming to `HTTPContextProviding` and returning the
|
||
`HTTPRequest` for a given JSON-RPC id while it is in flight.
|
||
|
||
### Graceful Shutdown
|
||
|
||
We recommend using
|
||
[Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle)
|
||
for managing startup and shutdown of services.
|
||
|
||
First, add the dependency to your `Package.swift`:
|
||
|
||
```swift
|
||
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0"),
|
||
```
|
||
|
||
Then implement the MCP server as a `Service`:
|
||
|
||
```swift
|
||
import MCP
|
||
import ServiceLifecycle
|
||
import Logging
|
||
|
||
struct MCPService: Service {
|
||
let server: Server
|
||
let transport: Transport
|
||
|
||
init(server: Server, transport: Transport) {
|
||
self.server = server
|
||
self.transport = transport
|
||
}
|
||
|
||
func run() async throws {
|
||
// Start the server
|
||
try await server.start(transport: transport)
|
||
|
||
// Keep running until external cancellation
|
||
try await Task.sleep(for: .days(365 * 100)) // Effectively forever
|
||
}
|
||
|
||
func shutdown() async throws {
|
||
// Gracefully shutdown the server
|
||
await server.stop()
|
||
}
|
||
}
|
||
```
|
||
|
||
Then use it in your application:
|
||
|
||
```swift
|
||
import MCP
|
||
import ServiceLifecycle
|
||
import Logging
|
||
|
||
let logger = Logger(label: "com.example.mcp-server")
|
||
|
||
// Create the MCP server
|
||
let server = Server(
|
||
name: "MyModelServer",
|
||
version: "1.0.0",
|
||
capabilities: .init(
|
||
prompts: .init(listChanged: true),
|
||
resources: .init(subscribe: true, listChanged: true),
|
||
tools: .init(listChanged: true)
|
||
)
|
||
)
|
||
|
||
// Add handlers directly to the server
|
||
await server.withMethodHandler(ListTools.self) { _ in
|
||
// Your implementation
|
||
return .init(tools: [
|
||
Tool(name: "example", description: "An example tool")
|
||
])
|
||
}
|
||
|
||
await server.withMethodHandler(CallTool.self) { params in
|
||
// Your implementation
|
||
return .init(content: [.text("Tool result")], isError: false)
|
||
}
|
||
|
||
// Create MCP service and other services
|
||
let transport = StdioTransport(logger: logger)
|
||
let mcpService = MCPService(server: server, transport: transport)
|
||
let databaseService = DatabaseService() // Your other services
|
||
|
||
// Create service group with signal handling
|
||
let serviceGroup = ServiceGroup(
|
||
services: [mcpService, databaseService],
|
||
configuration: .init(
|
||
gracefulShutdownSignals: [.sigterm, .sigint]
|
||
),
|
||
logger: logger
|
||
)
|
||
|
||
// Run the service group - this blocks until shutdown
|
||
try await serviceGroup.run()
|
||
```
|
||
|
||
This approach has several benefits:
|
||
|
||
- **Signal handling**:
|
||
Automatically traps SIGINT, SIGTERM and triggers graceful shutdown
|
||
- **Graceful shutdown**:
|
||
Properly shuts down your MCP server and other services
|
||
- **Timeout-based shutdown**:
|
||
Configurable shutdown timeouts to prevent hanging processes
|
||
- **Advanced service management**:
|
||
`[ServiceLifecycle](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/documentation/servicelifecycle)`
|
||
also supports service dependencies, conditional services,
|
||
and other useful features.
|
||
|
||
## Transports
|
||
|
||
MCP's transport layer handles communication between clients and servers.
|
||
The Swift SDK provides multiple built-in transports:
|
||
|
||
|
||
| Transport | Description | Platforms | Best for |
|
||
| --------- | ----------- | --------- | -------- |
|
||
| [`StdioTransport`](/Sources/MCP/Base/Transports/StdioTransport.swift) | Implements [stdio transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) using standard input/output streams | Apple platforms, Linux with glibc | Local subprocesses, CLI tools |
|
||
| [`HTTPClientTransport`](/Sources/MCP/Base/Transports/HTTPClientTransport.swift) | Implements [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) using Foundation's URL Loading System | All platforms with Foundation | Remote servers, web applications |
|
||
| [`StatelessHTTPServerTransport`](/Sources/MCP/Base/Transports/HTTPServer/StatelessHTTPServerTransport.swift) | HTTP server transport with simple request-response semantics; no session management or SSE streaming | All platforms with Foundation | Simple HTTP servers, serverless/edge functions |
|
||
| [`StatefulHTTPServerTransport`](/Sources/MCP/Base/Transports/HTTPServer/StatefulHTTPServerTransport.swift) | HTTP server transport with full session management and SSE streaming for server-initiated messages | All platforms with Foundation | Full-featured HTTP servers, streaming notifications |
|
||
| [`InMemoryTransport`](/Sources/MCP/Base/Transports/InMemoryTransport.swift) | Custom in-memory transport for direct communication within the same process | All platforms | Testing, debugging, same-process client-server communication |
|
||
| [`NetworkTransport`](/Sources/MCP/Base/Transports/NetworkTransport.swift) | Custom transport using Apple's Network framework for TCP/UDP connections | Apple platforms only | Low-level networking, custom protocols |
|
||
|
||
|
||
### Custom Transport Implementation
|
||
|
||
You can implement a custom transport by conforming to the `Transport` protocol:
|
||
|
||
```swift
|
||
import MCP
|
||
import Foundation
|
||
|
||
public actor MyCustomTransport: Transport {
|
||
public nonisolated let logger: Logger
|
||
private var isConnected = false
|
||
private let messageStream: AsyncThrowingStream<Data, any Swift.Error>
|
||
private let messageContinuation: AsyncThrowingStream<Data, any Swift.Error>.Continuation
|
||
|
||
public init(logger: Logger? = nil) {
|
||
self.logger = logger ?? Logger(label: "my.custom.transport")
|
||
|
||
var continuation: AsyncThrowingStream<Data, any Swift.Error>.Continuation!
|
||
self.messageStream = AsyncThrowingStream { continuation = $0 }
|
||
self.messageContinuation = continuation
|
||
}
|
||
|
||
public func connect() async throws {
|
||
// Implement your connection logic
|
||
isConnected = true
|
||
}
|
||
|
||
public func disconnect() async {
|
||
// Implement your disconnection logic
|
||
isConnected = false
|
||
messageContinuation.finish()
|
||
}
|
||
|
||
public func send(_ data: Data) async throws {
|
||
// Implement your message sending logic
|
||
}
|
||
|
||
public func receive() -> AsyncThrowingStream<Data, any Swift.Error> {
|
||
return messageStream
|
||
}
|
||
}
|
||
```
|
||
|
||
## Authentication
|
||
|
||
`HTTPClientTransport` supports OAuth 2.1 Bearer token authorization per the
|
||
[MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
|
||
When a server returns `401 Unauthorized` or `403 Forbidden`, the transport automatically:
|
||
|
||
1. Discovers Protected Resource Metadata (RFC 9728) at `/.well-known/oauth-protected-resource`
|
||
2. Discovers Authorization Server Metadata (RFC 8414 / OIDC Discovery 1.0)
|
||
3. Registers the client dynamically (RFC 7591) if needed
|
||
4. Acquires a Bearer token using the configured grant flow (PKCE enforced)
|
||
5. Retries the original request with the token attached
|
||
|
||
Authorization is opt-in and disabled by default.
|
||
Pass an `OAuthAuthorizer` to `HTTPClientTransport(authorizer:)` to enable it.
|
||
|
||
### Client: Client Credentials Flow
|
||
|
||
Machine-to-machine authentication using a pre-shared client secret:
|
||
|
||
```swift
|
||
let config = OAuthConfiguration(
|
||
grantType: .clientCredentials,
|
||
authentication: .clientSecretBasic(clientID: "my-app", clientSecret: "s3cr3t")
|
||
)
|
||
let authorizer = OAuthAuthorizer(configuration: config)
|
||
let transport = HTTPClientTransport(
|
||
endpoint: URL(string: "https://api.example.com/mcp")!,
|
||
authorizer: authorizer
|
||
)
|
||
let client = Client(name: "MyClient", version: "1.0.0")
|
||
try await client.connect(transport: transport)
|
||
```
|
||
|
||
### Client: Authorization Code Flow
|
||
|
||
Interactive, browser-based authentication with PKCE.
|
||
Implement `OAuthAuthorizationDelegate` to open the authorization URL and capture the redirect:
|
||
|
||
```swift
|
||
struct MyAuthDelegate: OAuthAuthorizationDelegate {
|
||
func presentAuthorizationURL(_ url: URL) async throws -> URL {
|
||
// Open the URL in a browser/webview and wait for the callback redirect URI.
|
||
// The returned URL must include the authorization code and state parameters.
|
||
return try await openBrowserAndWaitForCallback(url)
|
||
}
|
||
}
|
||
|
||
let config = OAuthConfiguration(
|
||
grantType: .authorizationCode,
|
||
authentication: .none(clientID: "my-app"),
|
||
authorizationDelegate: MyAuthDelegate()
|
||
)
|
||
let authorizer = OAuthAuthorizer(configuration: config)
|
||
let transport = HTTPClientTransport(
|
||
endpoint: URL(string: "https://api.example.com/mcp")!,
|
||
authorizer: authorizer
|
||
)
|
||
```
|
||
|
||
### Client: Custom Token Provider
|
||
|
||
Supply an externally acquired token (e.g., from a system credential store) via `accessTokenProvider`.
|
||
The SDK calls this closure after discovery completes. Return `nil` to fall back to the configured grant flow:
|
||
|
||
```swift
|
||
let config = OAuthConfiguration(
|
||
grantType: .clientCredentials,
|
||
authentication: .none(clientID: "my-app"),
|
||
accessTokenProvider: { context, session in
|
||
// context contains the discovered resource URI, token endpoint, scopes, etc.
|
||
return try await KeychainTokenStore.shared.loadToken(for: context.resource)
|
||
}
|
||
)
|
||
```
|
||
|
||
### Client: Custom Token Storage
|
||
|
||
By default, tokens are stored in memory and lost when the process exits.
|
||
To persist tokens across sessions, implement `TokenStorage` and pass it to `OAuthAuthorizer`:
|
||
|
||
```swift
|
||
final class KeychainTokenStorage: TokenStorage {
|
||
func save(_ token: OAuthAccessToken) {
|
||
// Encode and store token.value in the system Keychain
|
||
}
|
||
|
||
func load() -> OAuthAccessToken? {
|
||
// Load and decode token from the Keychain
|
||
return nil
|
||
}
|
||
|
||
func clear() {
|
||
// Delete from the Keychain
|
||
}
|
||
}
|
||
|
||
let authorizer = OAuthAuthorizer(
|
||
configuration: config,
|
||
tokenStorage: KeychainTokenStorage()
|
||
)
|
||
```
|
||
|
||
### Client: `private_key_jwt` Authentication
|
||
|
||
Authenticate to the token endpoint using an asymmetric key (RFC 7523).
|
||
The SDK provides a built-in ES256 helper for P-256 keys:
|
||
|
||
```swift
|
||
let config = OAuthConfiguration(
|
||
grantType: .clientCredentials,
|
||
authentication: .privateKeyJWT(
|
||
clientID: "my-app",
|
||
assertionFactory: { tokenEndpoint, clientID in
|
||
try OAuthConfiguration.makePrivateKeyJWTAssertion(
|
||
clientID: clientID,
|
||
tokenEndpoint: tokenEndpoint,
|
||
privateKeyPEM: myEC256PrivateKeyPEM // PEM-encoded P-256 private key
|
||
)
|
||
}
|
||
)
|
||
)
|
||
```
|
||
|
||
### Client: Endpoint Overrides
|
||
|
||
Skip automatic discovery by providing explicit endpoint URLs.
|
||
Useful when the server does not publish well-known metadata documents:
|
||
|
||
```swift
|
||
let config = OAuthConfiguration(
|
||
grantType: .clientCredentials,
|
||
authentication: .clientSecretBasic(clientID: "app", clientSecret: "secret"),
|
||
endpointOverrides: OAuthConfiguration.EndpointOverrides(
|
||
tokenEndpoint: URL(string: "https://auth.example.com/oauth/token")!
|
||
)
|
||
)
|
||
```
|
||
|
||
### Server: Serving Protected Resource Metadata
|
||
|
||
Per the MCP authorization specification, servers **MUST** serve Protected Resource Metadata
|
||
at `/.well-known/oauth-protected-resource` so clients can discover authorization server endpoints.
|
||
|
||
Use `ProtectedResourceMetadataValidator` as the first validator in your pipeline so that
|
||
unauthenticated discovery requests are handled before the bearer token check:
|
||
|
||
```swift
|
||
let metadata = OAuthProtectedResourceServerMetadata(
|
||
resource: "https://api.example.com",
|
||
authorizationServers: [URL(string: "https://auth.example.com")!],
|
||
scopesSupported: ["read", "write"]
|
||
)
|
||
let metadataValidator = ProtectedResourceMetadataValidator(metadata: metadata)
|
||
```
|
||
|
||
### Server: Validating Bearer Tokens
|
||
|
||
Use `BearerTokenValidator` to authenticate incoming requests.
|
||
Your `tokenValidator` closure **MUST** verify the token's `aud` claim to prevent
|
||
token substitution attacks where a token intended for another resource is replayed against your server:
|
||
|
||
```swift
|
||
let resourceIdentifier = URL(string: "https://api.example.com")!
|
||
|
||
let bearerValidator = BearerTokenValidator(
|
||
resourceMetadataURL: URL(string: "https://api.example.com/.well-known/oauth-protected-resource")!,
|
||
resourceIdentifier: resourceIdentifier,
|
||
tokenValidator: { token, request, context in
|
||
guard let claims = try? verifyAndDecodeJWT(token) else {
|
||
return .invalidToken(errorDescription: "Token verification failed")
|
||
}
|
||
// Pass audience and expiry to BearerTokenInfo; the SDK validates the
|
||
// audience claim against resourceIdentifier automatically.
|
||
return .valid(BearerTokenInfo(
|
||
audience: claims.audience,
|
||
expiresAt: claims.expiresAt
|
||
))
|
||
}
|
||
)
|
||
|
||
let pipeline = StandardValidationPipeline(validators: [
|
||
metadataValidator, // serves /.well-known/oauth-protected-resource unauthenticated
|
||
bearerValidator, // validates Bearer tokens on all other requests
|
||
AcceptHeaderValidator(mode: .sseRequired),
|
||
ContentTypeValidator(),
|
||
SessionValidator(),
|
||
])
|
||
```
|
||
|
||
## Platform Availability
|
||
|
||
The Swift SDK has the following platform requirements:
|
||
|
||
|
||
| Platform | Minimum Version |
|
||
| ------------------ | ---------------------------------------------------------------------------------------- |
|
||
| macOS | 13.0+ |
|
||
| iOS / Mac Catalyst | 16.0+ |
|
||
| watchOS | 9.0+ |
|
||
| tvOS | 16.0+ |
|
||
| visionOS | 1.0+ |
|
||
| Linux | Distributions with `glibc` or `musl`, including Ubuntu, Debian, Fedora, and Alpine Linux |
|
||
|
||
|
||
While the core library works on any platform supporting Swift 6
|
||
(including Linux), running a client or server requires a compatible transport.
|
||
|
||
## Debugging and Logging
|
||
|
||
Enable logging to help troubleshoot issues:
|
||
|
||
```swift
|
||
import Logging
|
||
import MCP
|
||
|
||
// Configure Logger
|
||
LoggingSystem.bootstrap { label in
|
||
var handler = StreamLogHandler.standardOutput(label: label)
|
||
handler.logLevel = .debug
|
||
return handler
|
||
}
|
||
|
||
// Create logger
|
||
let logger = Logger(label: "com.example.mcp")
|
||
|
||
// Pass to client/server
|
||
let client = Client(name: "MyApp", version: "1.0.0")
|
||
|
||
// Pass to transport
|
||
let transport = StdioTransport(logger: logger)
|
||
```
|
||
|
||
## Additional Resources
|
||
|
||
- [MCP Specification](https://modelcontextprotocol.io/specification/2025-11-25)
|
||
- [Protocol Documentation](https://modelcontextprotocol.io)
|
||
- [GitHub Repository](https://github.com/modelcontextprotocol/swift-sdk)
|
||
|
||
## Changelog
|
||
|
||
This project follows [Semantic Versioning](https://semver.org/).
|
||
For pre-1.0 releases,
|
||
minor version increments (0.X.0) may contain breaking changes.
|
||
|
||
For details about changes in each release,
|
||
see the [GitHub Releases page](https://github.com/modelcontextprotocol/swift-sdk/releases).
|
||
|
||
## License
|
||
|
||
This project is licensed under Apache 2.0 for new contributions, with existing code under MIT. See the [LICENSE](LICENSE) file for details.
|
||
|
||
[mcp]: https://modelcontextprotocol.io
|
||
[mcp-spec-2025-11-25]: https://modelcontextprotocol.io/specification/2025-11-25
|