
This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.
You can build an MCP server in Swift with the official Swift SDK, added through Swift Package Manager as the MCP product of modelcontextprotocol/swift-sdk. You create a Server, register handlers for ListTools and CallTool, and start it on a transport such as StdioTransport. It runs on Apple platforms and on Linux.
This guide builds a small stdio server, adds a tool, tests it with MCP Inspector and connects it to Claude Desktop. It also covers the HTTP transports, and what the SDK's Tier 3 status and protocol version mean before you depend on it.
Checked against: the Swift SDK README on main and at the 0.12.1 tag, plus the SDK source for Server, Tool and Value at 0.12.1, on 2 November 2026. eSEOspace did not compile or run this Swift code; the machine used had Swift 5.10, and the SDK requires Swift 6.0 or later.
Key Takeaways
- The official Swift SDK is Tier 3 on the official SDK list as of November 2026.
- The latest release is 0.12.1 (7 May 2026), though the
mainbranch was still being updated in late October 2026. - The README says it implements the 2025-11-25 spec; 0.12.1's supported versions run from 2024-11-05 to 2025-11-25, so 2026-07-28 is not supported.
- You need Swift 6.0+ (Xcode 16+) and macOS 13+ or a recent Linux distribution.
- Unlike some SDKs, you write the
tools/listandtools/callhandlers yourself and route by tool name. - In a stdio server, every log line must go to stderr; the README's debug logging example writes to stdout.
Prerequisites
- Swift 6.0 or later. The README lists "Swift 6.0+ (Xcode 16+)."
- macOS 13.0+, or Linux with glibc or musl (Ubuntu, Debian, Fedora and Alpine are named). iOS 16+, watchOS 9+, tvOS 16+ and visionOS 1+ are supported for the library too.
- Node.js, to run MCP Inspector.
- A client such as Claude Desktop.
If you are new to MCP servers, read how to build your first MCP server for the concepts first.
Step 1: Create the package
Create an executable package with swift package init --type executable. Then add the SDK in Package.swift. The dependency and product lines below come from the README; the version is the current release:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "Bookshop",
platforms: [.macOS(.v13)],
dependencies: [
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.12.1")
],
targets: [
.executableTarget(
name: "Bookshop",
dependencies: [
.product(name: "MCP", package: "swift-sdk")
]
)
]
)
The README's snippet still says from: "0.11.0". That resolves to the newest 0.x release below 1.0 too, but pinning 0.12.1 makes the minimum explicit. Because the SDK is pre-1.0 and Tier 3, read the release notes before you move to a new minor version.
Step 2: Write a stdio server with one tool
This bookshop server uses the Server, withMethodHandler and StdioTransport APIs from the README:
import MCP
let catalog = ["Dune": "Frank Herbert", "Neuromancer": "William Gibson"]
let server = Server(
name: "bookshop",
version: "1.0.0",
capabilities: .init(tools: .init(listChanged: false))
)
await server.withMethodHandler(ListTools.self) { _ in
.init(tools: [
Tool(
name: "search_books",
description: "Search the catalog by title or author. Returns matching titles.",
inputSchema: .object([
"type": "object",
"properties": .object([
"query": .object([
"type": "string",
"description": "Part of a title or author name"
])
]),
"required": .array(["query"])
])
)
])
}
await server.withMethodHandler(CallTool.self) { params in
switch params.name {
case "search_books":
guard let query = params.arguments?["query"]?.stringValue?.lowercased() else {
return .init(content: [.text("The 'query' argument is required.")], isError: true)
}
let hits = catalog
.filter { $0.key.lowercased().contains(query) || $0.value.lowercased().contains(query) }
.map { $0.key }
return .init(content: [.text(hits.isEmpty ? "No matches." : hits.joined(separator: "\n"))], isError: false)
default:
return .init(content: [.text("Unknown tool: \(params.name)")], isError: true)
}
}
let transport = StdioTransport()
try await server.start(transport: transport)
await server.waitUntilCompleted()
What each part does:
Server(name:version:capabilities:)creates the server and declares the tools capability.- The
ListToolshandler answerstools/list. EachToolcarries a name, a description and aninputSchemawritten as a JSON SchemaValue. - The
CallToolhandler answerstools/call. You switch onparams.nameand read arguments with accessors such asstringValue. .init(content:isError:)builds the result.isError: truetells the model the call failed, in words it can act on.StdioTransport()reads stdin and writes stdout.start(transport:)connects it.waitUntilCompleted()is a public method onServerin the 0.12.1 source. It waits for the server's message loop, which keeps the process alive until the client disconnects.
The README's Basic Server Setup calls start first and registers handlers after it. Registering handlers first, as above, means no request can arrive before a handler exists.
Step 3: Add more tools
Add each new tool to the array in the ListTools handler and add a matching case to the CallTool switch. Keep the two in sync; a tool that is listed but has no case returns your "Unknown tool" error.
Keep each tool narrow, with plain names and argument descriptions. The model only sees the name, the description and the schema.
If your tool list can change while the server runs, set listChanged: true and send a list-changed notification when it does. For a fixed list, leave it false.
Step 4: Test with MCP Inspector
Build with swift build -c release, then point the MCP Inspector CLI at the binary:
npx @modelcontextprotocol/inspector --cli .build/release/Bookshop \
--protocol-era legacy --method tools/list
npx @modelcontextprotocol/inspector --cli .build/release/Bookshop \
--protocol-era legacy --method tools/call \
--tool-name search_books --tool-arg query=dune
--protocol-era legacy makes the Inspector use the initialize handshake that this SDK speaks. Running npx @modelcontextprotocol/inspector with no flags opens the web UI. Our lesson on testing with MCP Inspector walks through both.
Step 5: Connect it to Claude Desktop
Add the built binary under mcpServers in claude_desktop_config.json. On macOS the file is ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"bookshop": {
"command": "/ABSOLUTE/PATH/TO/Bookshop/.build/release/Bookshop"
}
}
}
Restart Claude Desktop. Our MCP config generator writes the same entry for other clients.
Transports
The README lists these transports:
| Transport | What it does | Platforms |
|---|---|---|
StdioTransport | Stdio for local subprocesses and CLI tools | Apple platforms, Linux with glibc |
StatelessHTTPServerTransport | HTTP request and response, no sessions or SSE streaming | All platforms with Foundation |
StatefulHTTPServerTransport | HTTP with sessions and SSE streaming for server-initiated messages | All platforms with Foundation |
HTTPClientTransport | Client side of Streamable HTTP | All platforms with Foundation |
InMemoryTransport | Same-process client and server, for tests | All platforms |
NetworkTransport | TCP or UDP with Apple's Network framework | Apple platforms only |
Both HTTP server transports exist in the 0.12.1 source. The README describes the stateless one as suited to "serverless/edge functions." Handlers on either HTTP transport can read the original request, including headers such as Authorization, through Server.currentHandlerContext.
For long-running services, the README shows wrapping the server in an MCPService run by swift-service-lifecycle, which traps SIGINT and SIGTERM and shuts down cleanly. For a remote deployment walk-through, see our lesson on deploying a remote MCP server.
Common pitfalls
- Logging to stdout. The README's debugging example uses
StreamLogHandler.standardOutput. That is fine for a client, but in a stdio server any stdout output corrupts the JSON-RPC stream. UseStreamLogHandler.standardErrorinstead. - Swift 5.x toolchains. The SDK requires Swift 6.0+. Older Xcode or Linux toolchains will fail to resolve or build it.
- Expecting 2026-07-28. The 0.12.1 source lists 2025-11-25 as the newest supported version. A client that only speaks 2026-07-28 will not negotiate with it.
- Old schema examples. The 0.12.1 README showed tool properties as plain strings, such as
"location": .string("City name"), which is not valid JSON Schema. The current README neststypeanddescriptionobjects, as in the code above. - Forgetting to keep the process alive. Without
waitUntilCompleted()or a service group, a top-level script can exit right afterstart. - Relative paths in client config. Desktop clients start your binary from their own working directory.
Next step
Continue with the free MCP course to add resources, prompts and OAuth. If you want a Swift or Apple-platform MCP integration built for your product, see our MCP development service. The official MCP SDKs overview compares all ten languages by tier.
More in the MCP Guide
Free tools: server.json Validator · MCP Error Code Lookup
Latest research: State of MCP: October 2026
Frequently asked questions
Is there an official Swift SDK for MCP?
Yes. The Swift SDK lives in the modelcontextprotocol GitHub organization and is listed on the official SDK page as Tier 3 as of November 2026. Tier 3 carries no minimum conformance score and no response-time commitments.
Can I run a Swift MCP server on Linux?
Yes. The README lists Linux distributions with glibc or musl, including Ubuntu, Debian, Fedora and Alpine. Note that StdioTransport lists "Linux with glibc," so check your transport on musl-based images such as Alpine.
Can I build an MCP server into an iOS app?
The library supports iOS 16 and later, but a server needs a transport its client can reach. Desktop clients launch local servers as stdio subprocesses, which does not fit an iOS app, so on iOS the SDK is more naturally used as a client or for in-process testing.
Does the Swift SDK support the 2026-07-28 spec?
Not as of release 0.12.1. Its supported protocol versions are 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25. Watch the repository's releases for updates.
Why does my Swift server show no tools in the client?
Check that you declared the tools capability, that the ListTools handler is registered, and that nothing writes to stdout. Then run tools/list through the Inspector CLI to see exactly what the server returns.











