SwiftMCP
A Swift implementation of the MCP (Model Context Protocol) for JSON-RPC over various transports.
Features
- Multiple transport options
- JSON-RPC 2.0 compliant with OpenAPI generation
- Built-in authorization and optional OAuth validation
- Transparent OAuth proxy mode for MCP and OpenAPI
- Cross-platform compatibility
Server Features
- Tools – expose functions with
@MCPTool - Resources – publish data with
@MCPResource - Prompts – build prompts using
@MCPPrompt - Utilities
RequestContext.current
- Structured logging streamed through Session.current
- Completion suggestions for parameters (defaults for Bool and CaseIterable)
Client Features
- Roots – dynamic filesystem roots announced by the client
- Sampling – request small previews of client files
Installation
CI builds and tests with Swift 6.4 across macOS, iOS, Linux, Windows, and Android. Apple CI lanes use Xcode 27.1; Linux, Windows, and Android use Swift 6.4.2.
Add SwiftMCP as a dependency in your Package.swift:
dependencies: [
.package(url: "https://github.com/Cocoanetics/SwiftMCP.git", branch: "main")
]
Package traits
SwiftMCP exposes its optional feature areas as SwiftPM package traits, all enabled by default — existing consumers and a plain swift build are unaffected. The single import SwiftMCP module is unchanged.
| Trait | Enables | Extra dependencies |
| --- | --- | --- |
| Server | HTTP/SSE + TCP server transports | swift-nio, swift-crypto, swift-certificates |
| Client | The MCP client (MCPServerProxy) | — |
| OpenAPI | OpenAPI / AI-plugin manifest models and routes | — |
The tool/schema foundation — JSONValue, JSONSchema, MCPToolProviding, the @MCPTool / @MCPServer macros, errors — is always-on core and needs no trait.
Disabling Server drops swift-nio (and swift-crypto/swift-certificates) entirely, which makes the package build on Windows and gives client/tools-only consumers a lighter graph. A consumer that only talks to remote MCP servers selects just what it needs (your manifest must be swift-tools-version: 6.1 or newer to choose traits):
.package(
url: "https://github.com/Cocoanetics/SwiftMCP.git",
branch: "main",
traits: ["Client"] // tool/schema core + client, no swift-nio
)
AppIntents bridging (@MCPAppIntentTool, AppShortcuts discovery) is not a trait: it is already gated by#if canImport(AppIntents), so it is automatically excluded on platforms without AppIntents (Linux, Windows, Android).
Usage
Command Line Demo
The included demo application shows how to use SwiftMCP with different transport options:
# Using stdio transport
SwiftMCPDemo stdio
Using HTTP+SSE transport
SwiftMCPDemo httpsse --port 8080
Using HTTP+SSE with authorization
SwiftMCPDemo httpsse --port 8080 --token your-secret-token
Using HTTP+SSE with OpenAPI support
SwiftMCPDemo httpsse --port 8080 --openapi
Using HTTP+SSE with authorization and OpenAPI support
SwiftMCPDemo httpsse --port 8080 --token your-secret-token --openapi
Using HTTP+SSE with OAuth (JSON config)
SwiftMCPDemo httpsse --port 8080 --oauth oauth-config-example.json
Using TCP+Bonjour transport
SwiftMCPDemo tcp --name "SwiftMCP Demo"
Using HTTP+SSE plus TCP+Bonjour in parallel
SwiftMCPDemo httpsse --port 8080 --tcp
When using HTTP+SSE transport with the --token option, clients must include an Authorization header with their requests:
Authorization: Bearer your-secret-token
OpenAPI support
The --openapi option enables OpenAPI endpoints for AI plugin integration. When this option is used, the server will provide an OpenAPI specification at /openapi.json and an AI plugin manifest at /.well-known/ai-plugin.json. This allows for easy integration with AI models and other tools that support OpenAPI.
OAuth support
MCP OAuth conformance helpers (OAuthConformanceError, OAuthAuthorizationResponse, OAuthClientRegistrationValidator,
OAuthClientCredentialStore, ClientIDMetadataDocument): authorization-response iss validation (RFC 9207 / SEP-2468),
mandatory application_type on dynamic client registration (SEP-837, enforced by the proxy's /oauth/register),
issuer-bound client credentials that force re-registration when the issuer changes (SEP-2352), and Client ID Metadata
Documents. Dynamic client registration is deprecated in favor of CIMD; it remains available as a fallback.
When HTTPSSETransport is configured with `OAuthConfiguration, the transport
validates incoming bearer tokens through the configured OAuth provider.
Validation can use an introspection endpoint or by checking JWT claims against
the provider's JWKS when introspection is not available. The transport also
serves metadata at /.well-known/oauth-authorization-server and
/.well-known/oauth-protected-resource for discovery by clients.
Progress, Logging, Roots and Sampling
SwiftMCP can send progress updates while a tool is running, stream structured
log messages to connected clients, handle dynamic filesystem roots announced
by the client and request small data samples for preview. See the demo server for
simple implementations of these features. The
Protocol Revisions
SwiftMCP serves both MCP eras side by side:
- Legacy (2025-03-26
,2025-06-18,2025-11-25): sessions opened with
handshake.
- Modern (
2026-07-28): stateless requests that carry the version, identity and capabilities in _meta, discovered through server/discover.
Servers advertise
2026-07-28 by default: server/discover and the -32004
unsupported-version error list MCPProtocolVersion.supported, newest first.
Legacy initialize only negotiates the legacy revisions, so a client that
proposes 2026-07-28 (or anything unknown) there still gets 2025-11-25.
To stay legacy-only, opt out on the server.
server/discover then answers
-32601, which discover-first clients treat as "fall back to initialize":
@MCPServer
actor MyServer {
nonisolated var advertisesModernEra: Bool { false }
}
On the client,
MCPServerProxy.eraPreference picks the era: .auto (the
default: probe server/discover, fall back to initialize), .legacy
(initialize only, no probe) or .modern (modern only).
To steer the revision itself, set
preferredProtocolVersion. A legacy revision
is proposed in initialize without a probe (the server may still answer with
an older one; negotiatedProtocolVersion holds the result). A modern revision
is selected from server/discover, with .auto still falling back to
initialize. connect() throws if the two settings contradict each other.
let proxy = MCPServerProxy(config: config)
await proxy.setPreferredProtocolVersion("2025-06-18")
try await proxy.connect()
Server pushes work in both eras. On a modern connection the proxy holds one
subscriptions/listen request open — a long-lived SSE stream over HTTP, the
connection itself over stdio and TCP — whose filter follows the registered
list-changed handlers and subscribeResource(uri:). subscribeResource and
unsubscribeResource update that filter and return once the server has
acknowledged it.
Migrating
- Servers need no changes. Opt out with
advertisesModernEra == false if a host must keep seeing a legacy-only server.
- Clients probe
server/discover before initialize. A legacy server answers with an error and the client falls back, but a stdio server that
ignores unknown methods costs eraProbeTimeout (3 s) and a respawn. Set
eraPreference = .legacy for such servers, and for code that depends on
legacy session state (sessionID, negotiatedProtocolVersion, logging/setLevel).
- The modern era has no
ping. A dual-era server still answers a legacy ping at any time, even before initialize. On a modern connection
MCPServerProxy.ping() sends that legacy ping when server/discover listed
a legacy revision, and a server/discover round trip otherwise.
MCPProtocolVersion.supported now includes 2026-07-28. Code that used it as "what initialize negotiates" should use
MCPProtocolVersion.legacySupported instead.
Custom Server Implementation
To implement your own MCP server:
- Attach the
@MCPServer macro to a reference type like class or actor
Define your tools using @MCPTool attribute
Choose and configure a transport (stdio, HTTP+SSE, or TCP+Bonjour)
Example:
@MCPServer
class MyServer {
@MCPTool
func add(a: Int, b: Int) -> Int {
return a + b
}
}
// Using HTTP+SSE transport with authorization
let server = MyServer()
let transport = HTTPSSETransport(server: server, port: 8080)
// Optional: Add authorization
transport.authorizationHandler = { token in
guard let token = token, token == "your-secret-token" else {
return .unauthorized("Invalid token")
}
return .authorized
}
// Or configure OAuth validation
transport.oauthConfiguration = OAuthConfiguration(
issuer: URL(string: "https://example.com")!,
authorizationEndpoint: URL(string: "https://example.com/authorize")!,
tokenEndpoint: URL(string: "https://example.com/oauth/token")!,
introspectionEndpoint: URL(string: "https://example.com/oauth/introspect")!,
jwksEndpoint: URL(string: "https://example.com/.well-known/jwks.json")!,
audience: "your-api-identifier",
clientID: "client",
clientSecret: "secret"
)
try transport.run()
To serve TCP+Bonjour instead:
let server = MyServer()
let tcpTransport = TCPBonjourTransport(server: server)
try await tcpTransport.run()
Typed Client Proxy
SwiftMCP can generate a typed client proxy for any server. This proxy mirrors the
@MCPTool, @MCPResource, and @MCPPrompt signatures and forwards calls through
MCPServerProxy. Enable it with generateClient: true on the server macro.
@MCPServer(generateClient: true)
actor Calculator {
@MCPTool
func add(a: Int, b: Int) -> Int {
a + b
}
}
let url = URL(string: "http://localhost:8080/mcp")!
let config = MCPServerConfig.sse(config: MCPServerSseConfig(url: url))
let proxy = MCPServerProxy(config: config)
try await proxy.connect()
let client = Calculator.Client(proxy: proxy)
let result = try await client.add(a: 2, b: 3)
For TCP+Bonjour discovery:
let config = MCPServerConfig.tcp(config: MCPServerTcpConfig(instanceName: "SwiftMCP Demo"))
let proxy = MCPServerProxy(config: config)
try await proxy.connect()
Discovery scope
Every MCP service advertises on the single service type
_mcp._tcp. Identity lives
in the Bonjour instance name, and how far that name reaches is one decision:
| scope | reach | advertised name for base
Post |
|---|---|---|
| .localUser (default) | loopback, name qualified by user | Post (oliver) |
| .localMachine | loopback, bare name | Post |
| .localNetwork | the attached link, bare name | Post |
Only
.localUser decorates the name, and only because the decoration is derivable
on both sides — client and server are the same user on the same machine. Nothing
qualifies by host: DNS-SD already resolves a service to its target host, and a
hostname changes when the machine is renamed. Two machines advertising the same
name are separated by mDNS, which renames the second to Post (2).
The scope drives binding, advertising, and browsing together, so a service can
never be advertised more widely than it is served. Client and server pass the same
base name and let the scope derive the rest:
// server
let transport = TCPBonjourTransport(server: server) // .localUser
// client
let config = MCPServerTcpConfig(instanceName: server.serverName) // .localUser
.localUser is the default because instance names are machine-wide: two accounts
running the same server would otherwise collide, and the second user's client would
silently connect to the first user's process. It prevents that mis-binding — it is
not an access boundary, since a loopback socket is visible to every account on the
machine. Authenticate if you need one.
.localNetwork is opt-in — the TCP transport has no authorization hook, so anything
reachable there is reachable unauthenticated unless you add your own layer.
When discovery isn't an option, connect directly and skip it:
let config = MCPServerTcpConfig(host: "127.0.0.1", port: 8181)
Notes:
- Client generation is opt-in (
generateClient defaults to false).
Client methods always throw to surface transport or server errors.
Prefer the Streamable HTTP /mcp endpoint for new clients. The legacy /sse endpoint remains available for backwards compatibility.
Client Notification Handlers
MCPServerProxy can also surface typed JSON-RPC notifications from the server.
Use setNotificationHandler(_:as:handler:) for custom app/domain events and
use setLogNotificationHandler(_:) / setProgressNotificationHandler(_:) for
the built-in convenience paths.
struct RunStatusUpdate: Codable, Sendable {
let runID: String
let status: String
}
await proxy.setNotificationHandler("notifications/runStatusChanged", as: RunStatusUpdate.self) { update in
print("Run \(update.runID) is now \(update.status)")
}
SwiftMCPUtility Proxy Generation
Use
SwiftMCPUtility generate-proxy to create a client proxy for any MCP server,
including non-SwiftMCP servers. The generated proxy calls MCPServerProxy under
the hood, generates typed tool methods from MCP tool schemas, and adds first-class
resource/prompt convenience APIs when the server advertises those capabilities.
Input parameters are inferred from the server's tool schemas, and only string
formats such as date-time, uri, uuid, and byte can be mapped to native
types (Date, URL, UUID, Data).
SwiftMCPUtility generate-proxy --sse http://localhost:8080/mcp -o ToolsProxy.swift
All generated methods are
async throws so transport and server errors are surfaced.
OpenAPI Return Types
If the MCP server exposes an OpenAPI JSON document, pass its URL (or file path)
to
--openapi to enrich return types. The generator overlays OpenAPI response
schemas onto the MCP tool list, produces Codable structs/enums for structured
responses, and uses those types in the proxy's return signatures.
SwiftMCPUtility generate-proxy \
--sse http://localhost:8080/mcp \
--openapi http://localhost:8080/openapi.json \
-o ToolsProxy.swift
The generated Codable types can be used as-is or replaced with your own models
later if you have local definitions.
Documentation Extraction
The
@MCPServer and @MCPTool macros extract documentation comments to describe class, parameters and return value.
Macros Functionality
The macros in this repository provide functionality for defining and exposing tools and servers in the SwiftMCP framework. Here are the main functionalities of the macros:
@MCPServer: This macro is used to define a class or actor as an MCP server. It extracts the server description from documentation comments (or the optional description: override). An example of its usage can be seen in the Demos/SwiftMCPDemo/Calculator.swift file, where the Calculator actor is annotated with @MCPServer(name: "SwiftMCP Demo").
@MCPTool: This macro is used to define functions within an MCP server that can be called as tools. It also extracts documentation comments to describe the function, parameters, and return values. Examples of its usage can be seen in the Demos/SwiftMCPDemo/Calculator.swift file, where various functions such as add, subtract, testArray, multiply, divide, greet, ping, and noop are annotated with @MCPTool.
@MCPResource: This macro is used to expose read-only data through URI templates. Resources allow clients to access data using structured URIs with path and query parameters. The macro automatically generates the necessary infrastructure to match URIs against templates and extract parameters.
Using @MCPResource
The
@MCPResource macro allows you to expose functions as resources that can be accessed via URI patterns:
@MCPServer(name: "ResourceServer")
actor ResourceServer {
/// Get user profile by ID
@MCPResource("users://{user_id}/profile")
func getUserProfile(user_id: Int) -> String {
return "Profile for user \(user_id)"
}
/// Search users with pagination
@MCPResource("users://search?q={query}&page={page}", mimeType: "application/json")
func searchUsers(query: String, page: Int = 1) -> String {
return """
{"query": "\(query)", "page": \(page), "results": [...]}
"""
}
}
Key features:
- URI Templates: Define patterns with placeholders in curly braces
{param}
Path Parameters: Extract values from the URI path (e.g., /users/{id})
Query Parameters: Extract values from query strings (e.g., ?page={page})
Optional Parameters: Support default values for optional parameters
MIME Types: Optionally specify content type with mimeType parameter
Type Safety: Parameters are automatically converted to the correct Swift types
The server automatically provides:
- Resource discovery through
mcpResourceTemplates`
License
This project is licensed under the BSD 2-clause License - see the LICENSE file for details.