Building a MCP Server in Swift: A Step-by-Step Guide

Peter LiddlePeter Liddle
11 min read

The Model Context Protocol (MCP) is a powerful standard that enables AI systems to connect with external tools and data sources. In this tutorial, we'll walk through creating a simple MCP server in Swift that provides two basic tools: an echo tool and a random number selector tool.

What is MCP?

MCP (Model Context Protocol) is a standard that allows AI systems like large language models (LLMs) to interact with external tools and services. By implementing an MCP server, you can extend AI capabilities with custom functionality that can be accessed through a standardized interface.

Prerequisites

Before we begin, make sure you have:

  • Xcode 15 or later
  • Swift 6.0 or later
  • Basic knowledge of Swift and SPM (Swift Package Manager)

Creating the Project

You can create a new Swift package either using Xcode or the command line. Choose the method you prefer:

Option 1: Creating the package in Xcode

  1. Open Xcode and select 'File > New > Package...'
  2. Name your package 'SwiftMCPServerExample'
  3. Select 'Executable' as the package type
  4. Choose a location to save your project and click 'Create'

Option 2: Creating the package via command line

mkdir SwiftMCPServerExample
cd SwiftMCPServerExample
swift package init --type executable

After creating the package via command line, you can open it in Xcode by double-clicking the Package.swift file or using xed . in the terminal.

Regardless of which method you choose, we'll be using Swift Package Manager (SPM) for dependency management and building/running the project directly in Xcode for the remainder of this tutorial.

Setting Up Dependencies

Next, we need to modify the Package.swift file to include the necessary dependencies:

// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SwiftMCPServerExample",
    platforms: [
        .macOS(.v13) // This is optional, specify if you have a minimum macOS version requirement
    ],
    dependencies: [
        // MCP Swift SDK - the core library for implementing the Model Context Protocol, imported through `mcp-swift-sdk-helpers`
        // Uncomment this line if you want to use the core SDK directly without the helper methods
        // .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"),

        // Service Lifecycle for managing the server lifecycle
        .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"),

        // MCP Helper package - provides utilities for encoding, decoding and producing JSON schemas
        // Comment this line if you want to use the core SDK directly without the helper methods
        .package(url: "https://github.com/glaciotech/mcp-swift-sdk-helpers.git", from: "0.1.0"),
    ],
    targets: [
        .executableTarget(
            name: "SwiftMCPServerExample",
            dependencies: [
                .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
                // Comment this line when using the helper package
                // .product(name: "MCP", package: "swift-sdk"),

                // Use the MCPHelpers product from our helper package
                .product(name: "MCPHelpers", package: "mcp-swift-sdk-helpers")
            ]
        ),
        .testTarget(
            name: "SwiftMCPServerExampleTests",
            dependencies: [
                "SwiftMCPServerExample",
                .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
                // Comment this line when using the helper package
                // .product(name: "MCP", package: "swift-sdk"),

                // Use the MCPHelpers product from our helper package
                .product(name: "MCPHelpers", package: "mcp-swift-sdk-helpers")
            ],
            path: "Tests"
        )
    ]
)

After updating the Package.swift file, Xcode should automatically resolve the dependencies. If not, you can reset the package cache by going to File > Packages > Reset Package Caches, or run this command in the terminal:

swift package resolve

This will download and resolve all the dependencies.

About the MCP Helper Package

In this tutorial, we're using an MCP Helper package that wraps the core modelcontextprotocol/swift-sdk and adds some helpful utilities:

  • JSON Schema Generation: Makes it easier to define the input/output schemas for your tools
  • Parameter Initialization: Simplifies parsing incoming parameters from the LLM
  • Type-Safe Models: Provides strongly-typed Swift models for your tool inputs and outputs

This helper package makes it much easier to create MCP servers by reducing boilerplate code and providing a more Swift-idiomatic API.

Creating the Project Structure

Now, let's create the files we need for our MCP server. In Xcode, right-click on the 'Sources' folder and select 'New File...' to create each of the following files:

  1. MCPServerFactory.swift - Factory for creating and configuring the MCP server
  2. MCPService.swift - Service for managing the MCP server lifecycle
  3. We'll also modify the existing main.swift file

Defining Tool Input Schemas and Enums

First, let's define the input schemas for our tools and an enum to keep track of our registered tools in MCPServerFactory.swift:

import Foundation
import SwiftyJsonSchema
import ServiceLifecycle
import MCP
import Logging
import MCPHelpers

// We make use of SwiftyJsonSchema to generate the JSON schema that informs the LLM of how to structure the tool call
struct EchoToolInput: ProducesJSONSchema, ParamInitializable {
    static let exampleValue = EchoToolInput(echoText: "Echo...")

    @SchemaInfo(description: "Text to send to the service that will be echoed back")
    var echoText: String = ""
}

struct PickRandomToolInput: ProducesJSONSchema, ParamInitializable {
    static let exampleValue = PickRandomToolInput(ints: [5, 4, 6, 7, 123, 8411])

    @SchemaInfo(description: "An array of integers to select one at random from")
    var ints: [Int]? = nil
}

enum RegisteredTools: String {
    case echo
    case selectRandom
}

Creating the MCP Server Factory

Next, let's implement the MCPServerFactory class that will create and configure our MCP server:

/// MCPSeverFactory creates and configures our server with the example Tools and Prompts
class MCPServerFactory {

    /// This creates a new instance of a configured MCP server with our Tools ready to go
    static func makeServer(with logger: Logger) async -> Server {
        // Create our server
        let server = Server(name: "ExampleSwiftMCPServer",
                            version: "1.0",
                            capabilities: .init(logging: Server.Capabilities.Logging(),
                                                resources: .init(subscribe: true, listChanged: true),
                                                tools: .init(listChanged: true)
        ))

        // Register the tools
        await registerTools(on: server)

        // Setup the code to handle the tool logic when we receive a request for that tool
        await server.withMethodHandler(CallTool.self, handler: toolsHandler)

        return server
    }
}

Creating and Implementing the Echo Tool

Now, let's implement the Echo tool. This tool simply returns whatever text is sent to it. Add the following to the MCPServerFactory class:

// Echo tool handler implementation
private static func echoHandler(_ text: String) -> String {
    return text
}

Creating and Implementing the Random Selector Tool

Next, let's implement the Random Selector tool. This tool takes an array of integers and returns a random element from the array:

// Random selector tool handler implementation
private static func pickRandomNumberHandler(_ numbers: [Int]?) -> Int {
    guard let numbers = numbers else {
        return .zero
    }
    return numbers.randomElement() ?? .zero
}

Registering the Tools with the MCP Server

Now, let's register our tools with the MCP server. Add this method to the MCPServerFactory class:

private static func registerTools(on server: Server) async {
    /// Register a tool list handler
    await server.withMethodHandler(ListTools.self) { _ in

        // Define 2 simple tools: an echo tool that just returns whatever the LLM sends
        // and a random selector tool that picks a random number from an array
        let tools = [
            Tool(
                name: RegisteredTools.echo.rawValue,
                description: "Echo back any text that was sent",
                inputSchema: try .produced(from: EchoToolInput.self)
            ),
            Tool(
                name: RegisteredTools.selectRandom.rawValue,
                description: "Takes in a collection of numbers or strings and picks one at random",
                inputSchema: try .produced(from: PickRandomToolInput.self)
            )
        ]
        return .init(tools: tools)
    }
}

Implementing the Tool Handler

Finally, let's implement the tool handler that processes incoming tool requests and routes them to the appropriate handler:

static func toolsHandler(params: CallTool.Parameters) async throws -> CallTool.Result {
    let unknownToolError = CallTool.Result(content: [.text("Unknown tool")], isError: true)

    // Convert tool name to our enum
    guard let tool = RegisteredTools(rawValue: params.name) else {
        return unknownToolError
    }

    switch tool {
    case RegisteredTools.echo:
        let input = try EchoToolInput(with: params)
        let result = echoHandler(input.echoText)
        return .init(
            content: [.text("You sent: \(result)")],
            isError: false
        )

    case RegisteredTools.selectRandom:
        let input = try PickRandomToolInput(with: params)
        let result = self.pickRandomNumberHandler(input.ints)
        return .init(
            content: [.text("I picked: \(result)")],
            isError: false
        )
    }
}

Creating the MCP Service

Now, let's create the MCPService.swift file that will handle the lifecycle of our MCP server:

import MCP
import ServiceLifecycle
import Logging

struct MCPService: Service {

    let server: Server
    let transport: StdioTransport

    init(server: Server, transport: StdioTransport) {
        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: .seconds(10000)) 
    }

    func shutdown() async throws {
        // Gracefully shutdown the server
        await server.stop()
    }
}

Setting Up the Main Application

Finally, let's implement the main.swift file to tie everything together:

import MCP
import ServiceLifecycle
import Foundation
import Logging

final class App: Sendable {

    // Create a logging object we'll hand to other parts of the project
    let log = {
        var logger = Logger(label: "tech.glacio.mcpserverexample")
        logger.logLevel = .debug
        return logger
    }()

    /// Create and start our MCP service, we make use of a ServiceGroup to handle launching and shutting down the server
    func start() async throws {

        log.info("App has started")

        // Create the configured server with registered Tools and the MCP service
        let transport = StdioTransport(logger: log)
        let server = await MCPServerFactory.makeServer(with: log)
        let mcpService = MCPService(server: server, transport: transport)

        // Create service group with signal handling
        let serviceGroup = ServiceGroup(services: [mcpService],
                                        gracefulShutdownSignals: [.sigterm, .sigint],
                                        logger: log)

        // Run the service group - this blocks until shutdown
        try await serviceGroup.run()
    }
}

// Start the app
try! await App().start()

Example of testing the tool call logic

To test our MCP server, let's create a simple test in Tests/MCPTest.swift:

import XCTest
import MCP
@testable import SwiftMCPServerExample
import Logging

final class MCPTest: XCTestCase {

    func testToolHandler() async throws {
        // Test the echo tool
        let echoToolParams = CallTool.Parameters(name: RegisteredTools.echo.rawValue, 
                                                arguments: ["echoText": "Test Text To Echo"])

        let echoResult = try await MCPServerFactory.toolsHandler(params: echoToolParams)
        XCTAssertEqual(echoResult.content.first?.text, "You sent: Test Text To Echo")
        XCTAssertFalse(echoResult.isError)

        // Test the random selector tool
        let testNumbers = [1, 2, 3, 4, 5]
        let randomToolParams = CallTool.Parameters(name: RegisteredTools.selectRandom.rawValue,
                                                  arguments: ["ints": testNumbers])

        let randomResult = try await MCPServerFactory.toolsHandler(params: randomToolParams)
        XCTAssertFalse(randomResult.isError)

        // Test with an unknown tool
        let unknownToolParams = CallTool.Parameters(name: "unknownTool", arguments: [:])
        let unknownResult = try await MCPServerFactory.toolsHandler(params: unknownToolParams)
        XCTAssertTrue(unknownResult.isError)
    }
}

Run the tests with:

swift test

Building the MCP Server

Before using your MCP server with Windsurf, you need to build it:

In Xcode:

  1. Select the 'SwiftMCPServerExample' scheme from the scheme selector
  2. Build the project by pressing Cmd+B

Alternatively, you can build from the terminal:

swift build

Note that running the MCP server directly doesn't do much on its own - it will just wait for input on stdin. The real power comes when Windsurf launches it automatically as an MCP plugin.

Testing and Using Your MCP Server in Windsurf

To use your MCP server with Windsurf (an AI development environment), you need to add it to your MCP Server configuration file. Once properly configured, Windsurf will automatically launch your MCP server when needed - you don't need to run it manually.

{
  "mcpServers": {
    "swift-mcp-server-example": {
      "command": "{XCODE_BUILD_FOLDER}/Products/Debug/SwiftMCPServerExample",
      "args": [],
      "env": {}
    }
  }
}

Replace {XCODE_BUILD_FOLDER} with your actual build folder path. In Xcode, you can find this by going to Product -> Copy Build Folder Path.

After adding this configuration, click the "Refresh" button in Windsurf to discover your new MCP Server. Windsurf will automatically launch your MCP server when needed. You can see the available MCP servers in Windsurf as shown in the screenshot below:

Screenshot: Windsurf interface showing available MCP servers including swift-mcp-server-example with 2 tools and mcp-server-firecrawl with 9 tools Windsurf MCP Server Configuration

Once your MCP server is running and connected to Windsurf, you can interact with it through Cascade. Here are examples of how to use the tools:

Using the Random Selector Tool

You can ask Cascade to generate a list of random numbers and use the selectRandom tool to pick one:

Screenshot: Using the selectRandom tool to pick a random number from a list of integers 1-10. The tool selected 7. MCP Server in Action - Random Selector Tool

Using the Echo Tool

You can also use the echo tool to have text echoed back to you:

Screenshot: Using the echo tool to have the text "Hello Windsurf" echoed back. MCP Server in Action - Echo Tool

As you can see from these examples, once your MCP server is properly configured in Windsurf, Cascade can seamlessly access and utilize your custom tools.

Extending Your MCP Server

Now that you have a basic MCP server working, you can extend it with more sophisticated tools. Here are some ideas:

  1. Add a weather data tool that fetches real-time weather information
  2. Create a calculator tool that can evaluate mathematical expressions
  3. Implement a translation tool that uses a third-party API
  4. Build a database query tool that can retrieve and manipulate data

Troubleshooting

If you encounter issues with your MCP server in Windsurf:

  1. Make sure you've built the project first
  2. Try closing Windsurf
  3. Kill any running MCP server processes in Activity Monitor
  4. Relaunch Windsurf

You can also try these additional troubleshooting steps:

  • Edit the MCP config file: You can disable problematic MCP servers by commenting them out in your MCP configuration file or adding a "disabled": true property to the server configuration. The MCP config can be accessed by going to Windsurf Settings → Cascade -> Manage Plugins and clicking on the "View raw config" button.
{
  "mcpServers": {
    "swift-mcp-server-example": {
      "command": "{XCODE_BUILD_FOLDER}/Products/Debug/SwiftMCPServerExample",
      "args": [],
      "env": {},
      "disabled": true  // Add this line to disable the server
    }
  }
}
  • View MCP server errors: You can check for errors in your MCP server by going to Windsurf Settings → Cascade -> Manage Plugins. This section displays the status of each MCP server and any error messages that might help diagnose issues.

Finding the Code on GitHub

The complete code for this tutorial is available on GitHub at github.com/glaciotech/SwiftMCPServerExample. Feel free to clone it, modify it, and use it as a starting point for your own MCP server projects.

Conclusion

In this tutorial, we've built a simple MCP server in Swift that provides two tools: an echo tool and a random number selector. We've learned how to:

  1. Set up a Swift package with the necessary dependencies
  2. Define tool input schemas using SwiftyJsonSchema
  3. Create and configure an MCP server
  4. Implement tool handlers
  5. Test the MCP server
  6. Integrate the server with Windsurf

MCP servers provide a powerful way to extend AI capabilities with custom tools and services. With the knowledge gained from this tutorial, you can now build more sophisticated MCP servers to meet your specific needs.

Happy coding!

0
Subscribe to my newsletter

Read articles from Peter Liddle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Peter Liddle
Peter Liddle