Creating MCP Servers in Swift

•7 min read•Loading views
Creating MCP Servers in Swift

App by the author

Nardy — Backgammon & Long Narde

Long Narde and classic backgammon for iPhone. Play a friend or a chatty AI, and on iPhone Duo the fold becomes the bar.

Model Context Protocol (MCP) is an open standard that defines how software applications can deliver contextual information to large language models (LLMs). It's intended for integration across a wide range of tools — such as code editors, IDEs, and other applications. By leveraging natural language, MCP allows seamless interaction with multiple tools and data sources.

In this article, we will create a simple MCP server in Swift that returns the current Swift version on your machine.

Creating a server

We'll start with a package:

mkdir swift-version-mcp
cd swift-version-mcp
swift package init --type executable

Next, add the official Swift SDK for Model Context Protocol servers and clients. Update the Package.swift file to include the SDK as a dependency:

Package.swift
// swift-tools-version:6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
 
import PackageDescription
 
let package = Package(
    name: "swift-version-mcp",
    platforms: [
        .macOS(.v13),
    ],
    products: [
        .executable(name: "swift-version-mcp", targets: ["SwiftVersionMCP"]),
    ],
    dependencies: [
        .package(url: "https://github.com/modelcontextprotocol/swift-sdk", from: "0.12.1"),
    ],
    targets: [
        .executableTarget(name: "SwiftVersionMCP", dependencies: [
            .product(name: "MCP", package: "swift-sdk")
        ]),
    ]
)

Open main.swift and add the following code:

main.swift
import MCP
import Foundation
 
let server = Server(
    name: "Swift Version Server",
    version: "1.0.0",
    capabilities: .init(tools: .init(listChanged: false))
)
 
let transport = StdioTransport()
try await server.start(transport: transport)

Here we are creating a server with a name and version. The capabilities parameter is used to define the capabilities of the server. In this case, we are only defining the tools capability. To start the server, we are using the StdioTransport class, which is a transport layer for the server. It allows the server to communicate with the client via standard input and output.

Next, we need to implement the tools. The server will have one tool that returns the current Swift version:

main.swift
let tool = Tool(name: "swift_version",
                description: "Returns the current Swift version",
                inputSchema: .object([
                    "type": .string("object"),
                    "additionalProperties": .bool(false)
                ]))
 
await server.withMethodHandler(ListTools.self) { params in
    ListTools.Result(tools: [tool])
}

inputSchema is not optional. The specification requires a valid JSON Schema object, and since SDK 0.10.0 the parameter has no default value, so the code doesn't compile without it. Our tool takes no parameters, so the schema is an object that accepts nothing else.

Tool names are case-sensitive and should contain only letters, digits, _, -, and . — no spaces or other special characters. Both swift_version and swiftVersion are valid; the snake case here is a habit from other servers.

To make the server respond to the tool, we need to implement the withMethodHandler(CallTool.self) function. It will be called when the client requests the tool. The method should return a Result object with the result of the tool:

main.swift
await server.withMethodHandler(CallTool.self) { params in
    guard params.name == tool.name else {
        throw MCPError.invalidParams("Wrong tool name: \(params.name)")
    }
    let version = swiftVersion() ?? "No version"
    return CallTool.Result(content: [.text(text: version, annotations: nil, _meta: nil)])
}
 
func swiftVersion() -> String? {
    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
    process.arguments = ["swift", "--version"]
 
    let outputPipe = Pipe()
    process.standardOutput = outputPipe
 
    do {
        try process.run()
        process.waitUntilExit()
 
        let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
        return String(data: data, encoding: .utf8)
    } catch {
        return "Error running swift-version: \(error)"
    }
}

Because the server may contain several tools, we check the tool name here. The shorter .text("...") helper still works, but it's deprecated since 0.12.0 and warns on build.

In the end of the file, add a call to waitUntilCompleted() to keep the server running:

main.swift
await server.waitUntilCompleted()

Build the package with the following command to generate the executable:

swift build

Interfaces in the SDK look a bit strange, because it was highly inspired by TypeScript implementation. It uses the same naming conventions and patterns. Sometimes you need to use dictionaries without strong types, but it is not a big deal. But hopefully it supports structured concurrency, so you can use async/await syntax.

Since this post was written, the SDK covers the 2025-11-25 specification, ships StatelessHTTPServerTransport and StatefulHTTPServerTransport for servers that aren't local subprocesses, supports OAuth authentication, and adds tool titles, icons, outputSchema and structured content. Stdio is still the simplest way to start, though.

Now, we are ready to test the server. Agent mode in Visual Studio Code works with any MCP server, so let's configure it.

Using MCP servers in Visual Studio Code

Here is how to add the server. Make sure you install Github Copilot extension first.

  1. Open Command Palette... via ⌘+⇧+P shortcut and run MCP: Add Server
  2. Select a type. For a local executable it's Command (stdio); the other option is HTTP (HTTP or Server-Sent Events)
  3. Add a path to the executable:
/Users/artemnovichkov/Developer/swift-version-mcp/.build/arm64-apple-macosx/debug/swift-version-mcp
  1. Enter a server ID, for example, swift-version-server
  2. Select where to save it. Copilot Global writes to ~/.copilot/mcp-config.json and is available everywhere; the workspace-root .mcp.json is shared with the team. The older destinations, workspace .vscode/mcp.json and user-profile mcp.json, still work, but the guided flow marks them as deprecated.

After finishing the setup, you will see the server in the list:

{
    "mcpServers": {
        "swift-version-server": {
            "type": "stdio",
            "command": "/Users/artemnovichkov/Developer/swift-version-mcp/.build/arm64-apple-macosx/debug/swift-version-mcp",
            "args": []
        }
    }
}

That's the mcpServers format, the same one Cursor and Claude Desktop use. If you picked one of the VS Code files instead, the top-level key is servers.

Above the server name you will see a Start button. Click on it to start the server:

▷ Start | 1 cached tools

In the output you will see the following logs:

2025-04-16 23:13:15.438 [info] Starting server swift-version-server
2025-04-16 23:13:15.439 [info] Connection state: Starting
2025-04-16 23:13:15.439 [info] Starting server from LocalProcess extension host
2025-04-16 23:13:15.443 [info] Connection state: Starting
2025-04-16 23:13:15.448 [info] Connection state: Running
2025-04-16 23:13:15.482 [info] Discovered 1 tools

Now open a chat via ^+⌘+I shortcut. In the top left corner you will see Select Tools... button with a count of available tools. Click on it to see a list of available tools.

Chat

That's it! Now you can use the tool in the chat. Try to ask a question about Swift version and you will see a response from the server:

Result

Using MCP servers in other clients

You can use the server in other MCP clients like Cursor, Windsurf, Claude Desktop, etc. The configuration may vary, but the idea is the same.

Here is the configuration for Cursor in .cursor/mcp.json :

{
  "mcpServers": {
    "swift-version-server": {
      "type": "stdio",
      "command": "/Users/artemnovichkov/Developer/swift-version-mcp/.build/arm64-apple-macosx/debug/swift-version-mcp"
    }
  }
}

And here is the result:

Cursor

For Claude Desktop, you can use the same configuration as for Cursor, but you need to add the server in claude_desktop_config.json.

And here is the result:

Claude

Make sure to restart Claude Desktop after adding the server.

Conclusion

During the development of the server, I learned a lot about Model Context Protocol. I used to joke here about MCP integration in Xcode. It arrived in Xcode 26.3, and Xcode 27 moved external agents behind External Agent Access in Settings → Intelligence, so the tools stay reachable with Xcode closed. I wrote about that in Headless Xcode: From Prompt to Simulator with MCP.

The final code is available on Github.

References