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

Table of contents
- What is MCP?
- Prerequisites
- Creating the Project
- Setting Up Dependencies
- Creating the Project Structure
- Defining Tool Input Schemas and Enums
- Creating the MCP Server Factory
- Creating and Implementing the Echo Tool
- Creating and Implementing the Random Selector Tool
- Registering the Tools with the MCP Server
- Implementing the Tool Handler
- Creating the MCP Service
- Setting Up the Main Application
- Example of testing the tool call logic
- Building the MCP Server
- Testing and Using Your MCP Server in Windsurf
- Extending Your MCP Server
- Troubleshooting
- Finding the Code on GitHub
- Conclusion
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
- Open Xcode and select 'File > New > Package...'
- Name your package 'SwiftMCPServerExample'
- Select 'Executable' as the package type
- 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:
MCPServerFactory.swift
- Factory for creating and configuring the MCP serverMCPService.swift
- Service for managing the MCP server lifecycle- 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:
- Select the 'SwiftMCPServerExample' scheme from the scheme selector
- 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
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.
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.
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:
- Add a weather data tool that fetches real-time weather information
- Create a calculator tool that can evaluate mathematical expressions
- Implement a translation tool that uses a third-party API
- Build a database query tool that can retrieve and manipulate data
Troubleshooting
If you encounter issues with your MCP server in Windsurf:
- Make sure you've built the project first
- Try closing Windsurf
- Kill any running MCP server processes in Activity Monitor
- 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:
- Set up a Swift package with the necessary dependencies
- Define tool input schemas using SwiftyJsonSchema
- Create and configure an MCP server
- Implement tool handlers
- Test the MCP server
- 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!
Subscribe to my newsletter
Read articles from Peter Liddle directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
