Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 107 additions & 2 deletions App/Controllers/ServerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ enum ServiceRegistry {
CalendarService.shared,
CaptureService.shared,
ContactsService.shared,
FilesService.shared,
LocationService.shared,
MapsService.shared,
MessageService.shared,
Expand All @@ -64,6 +65,7 @@ enum ServiceRegistry {
calendarEnabled: Binding<Bool>,
captureEnabled: Binding<Bool>,
contactsEnabled: Binding<Bool>,
filesEnabled: Binding<Bool>,
locationEnabled: Binding<Bool>,
mapsEnabled: Binding<Bool>,
messagesEnabled: Binding<Bool>,
Expand Down Expand Up @@ -93,6 +95,13 @@ enum ServiceRegistry {
service: ContactsService.shared,
binding: contactsEnabled
),
ServiceConfig(
name: "Files",
iconName: "folder.fill",
color: .indigo,
service: FilesService.shared,
binding: filesEnabled
),
ServiceConfig(
name: "Location",
iconName: "location.fill",
Expand Down Expand Up @@ -149,6 +158,7 @@ final class ServerController: ObservableObject {
@AppStorage("calendarEnabled") private var calendarEnabled = false
@AppStorage("captureEnabled") private var captureEnabled = false
@AppStorage("contactsEnabled") private var contactsEnabled = false
@AppStorage("finderEnabled") private var finderEnabled = false
@AppStorage("locationEnabled") private var locationEnabled = false
@AppStorage("mapsEnabled") private var mapsEnabled = true // Default for maps
@AppStorage("messagesEnabled") private var messagesEnabled = false
Expand All @@ -165,6 +175,7 @@ final class ServerController: ObservableObject {
calendarEnabled: $calendarEnabled,
captureEnabled: $captureEnabled,
contactsEnabled: $contactsEnabled,
filesEnabled: $finderEnabled,
locationEnabled: $locationEnabled,
mapsEnabled: $mapsEnabled,
messagesEnabled: $messagesEnabled,
Expand Down Expand Up @@ -415,6 +426,7 @@ actor MCPConnectionManager {
name: Bundle.main.name ?? "iMCP",
version: Bundle.main.shortVersionString ?? "unknown",
capabilities: MCP.Server.Capabilities(
resources: .init(subscribe: false, listChanged: true),
tools: .init(listChanged: true)
)
)
Expand Down Expand Up @@ -853,12 +865,49 @@ actor ServerNetworkManager {
return ListPrompts.Result(prompts: [])
}

// Register the resources/list handler
// Register the resources/list handler (empty for now since we use templates)
await server.withMethodHandler(ListResources.self) { _ in
log.debug("Handling ListResources request for \(connectionID)")
return ListResources.Result(resources: [])
}

// Register the resources/templates/list handler
await server.withMethodHandler(ListResourceTemplates.self) { [weak self] _ in
guard let self = self else {
return ListResourceTemplates.Result(templates: [])
}

log.debug("Handling ListResourceTemplates request for \(connectionID)")

var templates: [MCP.Resource.Template] = []
if await self.isEnabledState {
for service in await self.services {
let serviceId = String(describing: type(of: service))

// Get the binding value in an actor-safe way
if let isServiceEnabled = await self.serviceBindings[serviceId]?.wrappedValue,
isServiceEnabled
{
for template in service.resourceTemplates {
log.debug("Adding resource template: \(template.name)")
templates.append(
.init(
uriTemplate: template.uriTemplate,
name: template.name,
description: template.description,
mimeType: template.mimeType
)
)
}
}
}
}

log.info(
"Returning \(templates.count) available resource templates for \(connectionID)")
return ListResourceTemplates.Result(templates: templates)
}

// Register tools/list handler
await server.withMethodHandler(ListTools.self) { [weak self] _ in
guard let self = self else {
Expand All @@ -877,7 +926,6 @@ actor ServerNetworkManager {
isServiceEnabled
{
for tool in service.tools {
log.debug("Adding tool: \(tool.name)")
tools.append(
.init(
name: tool.name,
Expand Down Expand Up @@ -975,6 +1023,63 @@ actor ServerNetworkManager {
isError: true
)
}

// Register resources/read handler
await server.withMethodHandler(ReadResource.self) { [weak self] params in
guard let self = self else {
return ReadResource.Result(
contents: [.text("Server unavailable", uri: params.uri)]
)
}

log.notice("Resource read received from \(connectionID): \(params.uri)")

guard await self.isEnabledState else {
log.notice("Resource read rejected: iMCP is disabled")
return ReadResource.Result(
contents: [
.text(
"iMCP is currently disabled. Please enable it to read resources.",
uri: params.uri)
]
)
}

for service in await self.services {
let serviceId = String(describing: type(of: service))

// Get the binding value in an actor-safe way
if let isServiceEnabled = await self.serviceBindings[serviceId]?.wrappedValue,
isServiceEnabled
{
do {
guard
let content = try await service.read(resource: params.uri)
else {
continue
}

log.notice("Resource \(params.uri) read successfully for \(connectionID)")

return ReadResource.Result(contents: [content])
} catch {
log.error(
"Error reading resource \(params.uri): \(error.localizedDescription)")
return ReadResource.Result(
contents: [.text("Error: \(error)", uri: params.uri)]
)
}
}
}

log.error("Resource not found or service not enabled: \(params.uri)")
return ReadResource.Result(
contents: [
.text(
"Resource not found or service not enabled: \(params.uri)", uri: params.uri)
]
)
}
}

// Update the enabled state and notify clients
Expand Down
29 changes: 29 additions & 0 deletions App/Models/Resource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation
import MCP
import Ontology

struct ResourceTemplate: Sendable {
let name: String
let description: String?
let uriTemplate: String
let mimeType: String?
private let reader: @Sendable (String) async throws -> ResourceContent?

init(
name: String,
description: String? = nil,
uriTemplate: String,
mimeType: String? = nil,
reader: @Sendable @escaping (String) async throws -> ResourceContent?
) {
self.name = name
self.description = description
self.uriTemplate = uriTemplate
self.mimeType = mimeType
self.reader = reader
}

func read(uri: String) async throws -> ResourceContent? {
try await reader(uri)
}
}
39 changes: 37 additions & 2 deletions App/Models/Service.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import MCP

public typealias ResourceContent = MCP.Resource.Content

@preconcurrency
protocol Service {
@ToolBuilder var tools: [Tool] { get }

var isActivated: Bool { get async }
func activate() async throws

@ResourceTemplateBuilder var resourceTemplates: [ResourceTemplate] { get }
@ToolBuilder var tools: [Tool] { get }
}

// MARK: - Default Implementation

extension Service {
// MARK: - Activation

var isActivated: Bool {
get async {
return true
Expand All @@ -15,6 +24,23 @@ extension Service {

func activate() async throws {}

// MARK: - Resources

var resourceTemplates: [ResourceTemplate] { [] }

func read(resource uri: String) async throws -> ResourceContent? {
for template in resourceTemplates {
if let content = try await template.read(uri: uri) {
return content
}
}
return nil
}

// MARK: - Tools

var tools: [Tool] { [] }

func call(tool name: String, with arguments: [String: Value]) async throws -> Value? {
for tool in tools where tool.name == name {
return try await tool.callAsFunction(arguments)
Expand All @@ -24,9 +50,18 @@ extension Service {
}
}

// MARK: - Builders

@resultBuilder
struct ToolBuilder {
static func buildBlock(_ tools: Tool...) -> [Tool] {
tools
}
}

@resultBuilder
struct ResourceTemplateBuilder {
static func buildBlock(_ templates: ResourceTemplate...) -> [ResourceTemplate] {
templates
}
}
2 changes: 1 addition & 1 deletion App/Models/Tool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public struct Tool: Sendable {
let annotations: MCP.Tool.Annotations
private let implementation: @Sendable ([String: Value]) async throws -> Value

public init<T: Encodable>(
public init<T: Encodable & Sendable>(
name: String,
description: String,
inputSchema: JSONSchema,
Expand Down
Loading