It’s 5 months since the release of SwiftMCP 1.0 and I’ve been slow cooking some enhancements for it. It was rewarding to see a bit of interested in this package, judging by issues and forks I could see on GitHub. Today, I’m revealing the work for the client-side I’ve done during this time.
The first version of SwiftMCP naturally focussed entirely on the server-side. You can annotate any class or actor with @MCPServer and that would expose its functions with the @MCPTool attribute via MCP. Metadata is gleaned from the documentation comments for the function and parameters. This means – as a developer – you don’t have to spend any extra effort to expose tools for your app via MCP. You only have to think what API surface you wish to expose and then design the functions such that they have easy to understand and documented prototypes.
SwiftMCP takes the metadata it derives directly from your code via macros and generates API responses. The initialize response contains all MCP tool descriptions, and their input schemas. Because of where MCP comes from – providing text input to LLMs – no thought seems to have been given to the idea that there could be structured responses. Therefore MCP spec is missing output schemas.
In contrast the OpenAPI spec does have output schemas as well as a degree of support for error responses. This will become relevant further down.
The Case for the Client
Despite this limitation MCP has become the de factor standard for JSON RPC function calling. If you are designing an API for an LLM, why not also call those tools from your own programs?
Before today, if you wanted to call into any MCP Server you would have to construct JSON messages and send these off to your server. Maybe some open source framework would make this slightly less bothersome, but it’s still not very “Swift-y”.
Having said that, it is worth mentioning that SwiftMCP also will serve the tools via OpenAPI JSON RPC function calling, if you enable this feature. You could then use a proxy generator what builds you Swift types and functions. Then you could conveniently interact with this local code.
That got me thinking: I do have all meta information within the @MCPServer macro. So at least for the code that you control you could generate the client-side proxy. For arbitrary other MCP Servers you could at least generate client code with the JSON types from the input schemas.
Several common types get mapped to JSON strings which an additional format specifier. For example Date gets mapped to String with format date-time. So for MCP Servers that tell us the format we can derive the native Swift types from this. Other types with similar treatment are URL, UUID and Data.
At this time the only way to get the native output/return types is to have a matching OpenAPI spec for the MCP Server and “steal” the types from there. Static types for custom errors are much harder to pull off, so that was out-of-scope for now.
Scenario 1 – You Control the Code for Server and Client
If you control both sides of the equation, then you are most lucky. The @MCPServer macro has gained a new ability to generat client-code for you. You awake the sleeper agent by adding `generateClient: true` to the macro. This causes it to generated a nested Client type within the type.
@MCPServer(generateClient: true)
actor Calculator {
@MCPTool
func add(a: Int, b: Int) -> Int {
a + b
}
}
Then to connect to the client proxy, you specify a configuration for the transport and instantiate a proxy.
let url = URL(string: "http://localhost:8080/sse")!
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)
The client API surface will exactly match the original functions with one exception: All functions are now throws because a problem could occur on the client with communicating with the server.
This path will retain all types, including Int and Double (which both get sent as number in JSON) and also struct. All types that are Codable and Sendable can be used.
This opens up the use case that you have your MCP code in a shared package to allow you importing the MCPServer both from your servers and clients, including structs you want to use as input or output.
Scenario 2 – You Control Only the Client Code
This is harder, but I got you covered nevertheless. To generate local proxy code for any arbitrary MCP Server there’s now a CLI tool. This connects to a server (over network or STDIO) and gets the tools schemas. You can compile the utility and call it as shown, or alternatively you can precede it with swift run from the project folder.
SwiftMCPUtility generate-proxy --sse http://localhost:8080/sse -o ToolsProxy.swift
This will generate the code for the local proxy. As stated above the limitation here is that certain types can only be modeled as native types if the inputSchema for string values has format information. Without extra information return types all have to be string.
If the server also serves an OpenAPI spec, then this can supplement the type information for output types.
SwiftMCPUtility generate-proxy \
--sse http://localhost:8080/sse \
--openapi http://localhost:8080/openapi.json \
-o ToolsProxy.swift
The proxy generator also creates nested struct declarations. Let’s look at one of those generated proxy functions in the ProxyDemoCLI demo.
/**
Custom description: Performs addition of two numbers
- Parameter a: First number to add
- Parameter b: Second number to add
- Returns: The sum of a and b
*/
public func add(a: Double, b: Double) async throws -> Double {
var arguments: [String: any Sendable] = [:]
arguments["a"] = a
arguments["b"] = b
let text = try await proxy.callTool("add", arguments: arguments)
return try MCPClientResultDecoder.decode(Double.self, from: text)
}
You can see how the arguments are put into the arguments dictionary and then the proxy’s callTool functions is called for the “add” command. This takes care of the type conversion as well as the JSON-RPC messaging. The response then gets converted back to the native type of Double.
Here’s another example from the demo that returns a custom struct.
public actor SwiftMCPDemoProxy {
/// A resource content implementation for files in the file system
public struct RandomFileResponseItem: Codable, Sendable {
/// The binary content of the resource (if it's a binary resource)
public let blob: Data?
/// The MIME type of the resource
public let mimeType: String?
/// The text content of the resource (if it's a text resource)
public let text: String?
/// The URI of the resource
public let uri: URL?
}
/**
A function returning a random file
- Returns: A multiple simple text files
*/
public func randomFile() async throws -> [RandomFileResponseItem] {
let text = try await proxy.callTool("randomFile")
return try MCPClientResultDecoder.decode([RandomFileResponseItem].self, from: text)
}
Here you can see that the proxy generator created a struct for the return type. The function randomFile decodes an array of this type from the JSON response. This way you get a statically typed Swift struct instead of a dictionary of arbitrary value types.
Since this is generated code you can actually further customize it and for example use your own struct declarations. You just have to replace the generated type with your own and this will work as long as it matches the JSON.
By contrast the same function as mentioned in scenario 1 is able to reference a public type and therefore no extra type declaration is necessary.
Conclusion
Please note that there might still be some rough edges as this functionality is brand new. If you find a problem or have a suggestion please let me know in the GitHub issues. How do you use SwiftMCP?
Categories: Administrative