Skip to content

Commit 975b805

Browse files
committed
feat(MCP): Add MCP Server
1 parent 9769eb7 commit 975b805

File tree

8 files changed

+346
-8
lines changed

8 files changed

+346
-8
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "2600"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "dep-checker-mcp"
19+
BuildableName = "dep-checker-mcp"
20+
BlueprintName = "dep-checker-mcp"
21+
ReferencedContainer = "container:">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
</TestAction>
33+
<LaunchAction
34+
buildConfiguration = "Debug"
35+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37+
launchStyle = "0"
38+
useCustomWorkingDirectory = "NO"
39+
ignoresPersistentStateOnLaunch = "NO"
40+
debugDocumentVersioning = "YES"
41+
debugServiceExtension = "internal"
42+
allowLocationSimulation = "YES">
43+
<BuildableProductRunnable
44+
runnableDebuggingMode = "0">
45+
<BuildableReference
46+
BuildableIdentifier = "primary"
47+
BlueprintIdentifier = "dep-checker-mcp"
48+
BuildableName = "dep-checker-mcp"
49+
BlueprintName = "dep-checker-mcp"
50+
ReferencedContainer = "container:">
51+
</BuildableReference>
52+
</BuildableProductRunnable>
53+
</LaunchAction>
54+
<ProfileAction
55+
buildConfiguration = "Release"
56+
shouldUseLaunchSchemeArgsEnv = "YES"
57+
savedToolIdentifier = ""
58+
useCustomWorkingDirectory = "NO"
59+
debugDocumentVersioning = "YES">
60+
<BuildableProductRunnable
61+
runnableDebuggingMode = "0">
62+
<BuildableReference
63+
BuildableIdentifier = "primary"
64+
BlueprintIdentifier = "dep-checker-mcp"
65+
BuildableName = "dep-checker-mcp"
66+
BlueprintName = "dep-checker-mcp"
67+
ReferencedContainer = "container:">
68+
</BuildableReference>
69+
</BuildableProductRunnable>
70+
</ProfileAction>
71+
<AnalyzeAction
72+
buildConfiguration = "Debug">
73+
</AnalyzeAction>
74+
<ArchiveAction
75+
buildConfiguration = "Release"
76+
revealArchiveInOrganizer = "YES">
77+
</ArchiveAction>
78+
</Scheme>

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ tests: setup-xcbeautify
2020
tests-ci:
2121
@echo "Running tests in CI..."
2222
swift test
23+
24+
# MCP
25+
debug-mcp:
26+
swift build --product dep-checker-mcp && npx @modelcontextprotocol/inspector $(shell pwd)/.build/arm64-apple-macosx/debug/dep-checker-mcp

Package.resolved

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,31 @@ let package = Package(
88
.executable(
99
name: "dep-checker",
1010
targets: ["CLI"]
11+
),
12+
.executable(
13+
name: "dep-checker-mcp",
14+
targets: ["MCPServer"]
1115
)
1216
],
1317
dependencies: [
1418
.package(url: "https://github.com/tuist/XcodeProj", exact: "9.5.0"),
1519
.package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.6.1"),
1620
.package(url: "https://github.com/ShawnBaek/Table.git", exact: "2.0.0"),
1721
.package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.57.2"),
22+
.package(url: "https://github.com/modelcontextprotocol/swift-sdk", exact: "0.10.2"),
1823
],
1924
targets: [
25+
.executableTarget(
26+
name: "MCPServer",
27+
dependencies: [
28+
.product(name: "MCP", package: "swift-sdk"),
29+
"Models",
30+
"Input",
31+
"DependencyChecker",
32+
"ProjectAnalyzer",
33+
"Validation"
34+
],
35+
),
2036
.executableTarget(
2137
name: "CLI",
2238
dependencies: [

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,22 @@ jobs:
8686
### Mint
8787
8888
```sh
89-
mint run amegias/ios-dep-checker --project-path ../MyProject/ --git-hub-token ${{ secrets.GITHUB_TOKEN }} --max-days 365
89+
mint run amegias/ios-dep-checker --project-path ../MyProject/
90+
```
91+
92+
### As Model Context Protocol (MCP) Server
93+
#### VS Code + Copilot (or similar)
94+
1. Open `mcp.json` config file.
95+
2. Add the `dep-checker-mcp` as a new MCP.
96+
```json
97+
{
98+
"servers": {
99+
"dep-checker-ios": {
100+
"command": "/.../dep-checker-mcp",
101+
"args": []
102+
}
103+
}
104+
}
90105
```
91106

92107
## Contributing

Sources/MCPServer/MCPServer.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Foundation
2+
import MCP
3+
import Models
4+
5+
@main
6+
struct MCPServer {
7+
static func main() async throws {
8+
let server = Server(
9+
name: "dep-checker-mcp",
10+
version: .appVersion,
11+
capabilities: .init(tools: .init(listChanged: true))
12+
)
13+
14+
await server.withMethodHandler(ListTools.self, handler: listToolsHandler)
15+
await server.withMethodHandler(CallTool.self, handler: callToolHandler)
16+
17+
let transport = StdioTransport()
18+
try await server.start(transport: transport)
19+
20+
await server.waitUntilCompleted()
21+
}
22+
}
23+
24+
private extension MCPServer {
25+
static func listToolsHandler(_ params: ListTools.Parameters) -> ListTools.Result {
26+
.init(tools: Tools.allCases.map(\.tool))
27+
}
28+
29+
static func callToolHandler(_ params: CallTool.Parameters) async -> CallTool.Result {
30+
guard let tools = Tools(rawValue: params.name) else {
31+
return .init(content: [.text("Unknown tool")], isError: true)
32+
}
33+
return await tools.callTool(args: params.arguments)
34+
}
35+
}

Sources/MCPServer/Tools.swift

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import DependencyChecker
2+
import Foundation
3+
import Input
4+
import MCP
5+
import Models
6+
import ProjectAnalyzer
7+
import Validation
8+
9+
enum Tools: String, CaseIterable {
10+
case depChecker = "dep-checker"
11+
12+
var tool: Tool {
13+
switch self {
14+
case .depChecker:
15+
Tool(
16+
name: rawValue,
17+
description: "Retrieves the dependencies of a Swift Package Manager or Xcode project and checks whether they are outdated, including how long they have been outdated.",
18+
inputSchema: .object([
19+
"type": .string("object"),
20+
"properties": .object([
21+
"gitHubToken": .object([
22+
"type": .string("string"),
23+
"description": .string(
24+
"GitHub token for retrieving data related to GitHub dependencies (optional)"
25+
)
26+
]),
27+
"projectPath": .object([
28+
"type": .string("string"),
29+
"description": .string(
30+
"Path to the project. It should point to the xcodeproj or Package.swift folder (required)"
31+
)
32+
]),
33+
"resolvedPackagePath": .object([
34+
"type": .string("string"),
35+
"description": .string(
36+
"Path to the Package.resolved folder. It will help to get the exact version of the resolved dependencies (optional)"
37+
)
38+
]),
39+
"includeDependencies": .object([
40+
"type": .string("string"),
41+
"description": .string("List of dependencies to include in the analysis (optional)")
42+
]),
43+
"excludeDependencies": .object([
44+
"type": .string("string"),
45+
"description": .string("List of dependencies to exclude from the analysis (optional)")
46+
]),
47+
"includeTransitiveDependencies": .object([
48+
"type": .string("string"),
49+
"description": .string(
50+
"Gets also the transitive dependencies found in resolved package files."
51+
)
52+
])
53+
]),
54+
"required": ["projectPath"]
55+
])
56+
)
57+
}
58+
}
59+
60+
func callTool(args: [String: Value]?) async -> CallTool.Result {
61+
switch self {
62+
case .depChecker:
63+
// MARK: - Inputs
64+
65+
guard let projectPathString = args?["projectPath"]?.stringValue,
66+
let projectPath = URL(string: projectPathString)
67+
else {
68+
return .init(content: [.text("projectPath is not valid")], isError: true)
69+
}
70+
71+
let gitHubToken = args?["gitHubToken"]?.stringValue
72+
73+
let resolvedPackagePath: URL? = if let resolvedPackagePathString = args?["resolvedPackagePath"]?
74+
.stringValue
75+
{
76+
URL(string: resolvedPackagePathString)
77+
} else {
78+
nil
79+
}
80+
81+
let includeDependencies: [String] = if let includeDependenciesString = args?["includeDependencies"]?
82+
.stringValue
83+
{
84+
includeDependenciesString.split(separator: " ").map(String.init)
85+
} else {
86+
[]
87+
}
88+
let excludeDependencies: [String] = if let excludeDependenciesString = args?["excludeDependencies"]?
89+
.stringValue
90+
{
91+
excludeDependenciesString.split(separator: " ").map(String.init)
92+
} else {
93+
[]
94+
}
95+
let includeTransitiveDependencies = if let includeTransitiveDependenciesString =
96+
args?["includeTransitiveDependencies"]?.stringValue
97+
{
98+
includeTransitiveDependenciesString == "true"
99+
} else {
100+
false
101+
}
102+
103+
do {
104+
let input = try InputCalculator().calculate(
105+
InlineInput(
106+
configFile: nil,
107+
gitHubToken: gitHubToken,
108+
maxDays: nil,
109+
outputFormat: .json,
110+
projectPath: projectPath,
111+
resolvedPackagePath: resolvedPackagePath,
112+
includeDependencies: includeDependencies,
113+
excludeDependencies: excludeDependencies,
114+
includeTransitiveDependencies: includeTransitiveDependencies
115+
)
116+
)
117+
118+
await Configuration.shared.configure(
119+
gitHubToken: input.gitHubToken
120+
)
121+
122+
// MARK: - Analyze the project
123+
124+
let dependencies = try ProjectAnalyzer().getDependencies(
125+
projectPath: input.projectPath,
126+
resolvedPackagePath: input.resolvedPackagePath,
127+
includeDependencies: input.includeDependencies,
128+
excludeDependencies: input.excludeDependencies,
129+
includeTransitiveDependencies: input.includeTransitiveDependencies
130+
)
131+
132+
// MARK: - Dependency checks
133+
134+
let results = await DependencyChecker().check(
135+
dependencies,
136+
maxDays: input.maxDays,
137+
maxDaysPerDependency: input.maxDaysPerDependency
138+
)
139+
140+
// MARK: - Result
141+
142+
return try .init(
143+
content: [.text(results.json()!)],
144+
isError: false
145+
)
146+
} catch {
147+
return .init(
148+
content: [.text("Error \(error.localizedDescription)")],
149+
isError: true
150+
)
151+
}
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)