Skip to content

Commit bb777e1

Browse files
authored
Merge pull request #1 from nedithgar:feat/debug-info
Feat/debug-info
2 parents aa33caf + 54eff56 commit bb777e1

File tree

3 files changed

+142
-21
lines changed

3 files changed

+142
-21
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ let package = Package(
3131
name: "ast-grep-mcp-swiftTests",
3232
dependencies: ["ast-grep-mcp-swift"]
3333
),
34-
]
34+
],
35+
swiftLanguageModes: [.v6]
3536
)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ Launch the MCP server over stdio:
4949

5050
```bash
5151
swift run ast-grep-mcp-swift --config /absolute/path/to/sgconfig.yaml
52+
swift run ast-grep-mcp-swift --verbose --config /absolute/path/to/sgconfig.yaml # verbose debug logs to stderr
53+
swift run ast-grep-mcp-swift --version # print version information
5254
```
5355

5456
You can omit `--config` if you rely on defaults or the `AST_GREP_CONFIG` environment variable.

Sources/ast-grep-mcp-swift/ast_grep_mcp_swift.swift

Lines changed: 138 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import MCP
44
import Yams
55

66
private let version = "0.1.0"
7+
enum DebugContext {
8+
@TaskLocal static var enabled = false
9+
}
10+
11+
private func debugLog(_ message: () -> String) {
12+
guard DebugContext.enabled else { return }
13+
if let data = ("[debug] " + message() + "\n").data(using: .utf8) {
14+
FileHandle.standardError.write(data)
15+
}
16+
}
717

818
private func resolveConfigPath(cliConfig: String?) throws -> String? {
919
if let cliConfig {
@@ -66,16 +76,91 @@ private struct CommandResult {
6676
let stderr: String
6777
}
6878

79+
/// Thread-safe accumulator for pipe output.
80+
final class OutputBuffer: @unchecked Sendable {
81+
private let lock = NSLock()
82+
private var data = Data()
83+
84+
func append(_ chunk: Data) {
85+
lock.withLock {
86+
data.append(chunk)
87+
}
88+
}
89+
90+
func snapshot() -> Data {
91+
lock.withLock {
92+
data
93+
}
94+
}
95+
}
96+
97+
/// Thread-safe helper to ensure a completion handler is called exactly once.
98+
final class OnceFlag: @unchecked Sendable {
99+
private let lock = NSLock()
100+
private var completed = false
101+
102+
/// Executes the given closure only on the first call; subsequent calls are no-ops.
103+
func callOnce(_ action: () -> Void) {
104+
lock.withLock {
105+
guard !completed else { return }
106+
completed = true
107+
action()
108+
}
109+
}
110+
}
111+
69112
private func runCommand(_ args: [String], input: String? = nil) throws -> CommandResult {
70113
let process = Process()
71114
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
72115
process.arguments = args
73116

117+
debugLog {
118+
var parts = ["Executing command:"]
119+
parts.append(args.joined(separator: " "))
120+
if input != nil {
121+
parts.append("(stdin provided)")
122+
}
123+
return parts.joined(separator: " ")
124+
}
125+
74126
let stdoutPipe = Pipe()
75127
let stderrPipe = Pipe()
76128
process.standardOutput = stdoutPipe
77129
process.standardError = stderrPipe
78130

131+
let stdoutBuffer = OutputBuffer()
132+
let stderrBuffer = OutputBuffer()
133+
134+
let group = DispatchGroup()
135+
let stdoutOnce = OnceFlag()
136+
let stderrOnce = OnceFlag()
137+
138+
group.enter()
139+
stdoutPipe.fileHandleForReading.readabilityHandler = { handle in
140+
let data = handle.availableData
141+
if data.isEmpty {
142+
stdoutOnce.callOnce {
143+
handle.readabilityHandler = nil
144+
group.leave()
145+
}
146+
return
147+
}
148+
stdoutBuffer.append(data)
149+
}
150+
151+
group.enter()
152+
stderrPipe.fileHandleForReading.readabilityHandler = { handle in
153+
let data = handle.availableData
154+
if data.isEmpty {
155+
stderrOnce.callOnce {
156+
handle.readabilityHandler = nil
157+
group.leave()
158+
}
159+
return
160+
}
161+
stderrBuffer.append(data)
162+
}
163+
79164
var stdinPipe: Pipe?
80165
if input != nil {
81166
let pipe = Pipe()
@@ -95,12 +180,18 @@ private func runCommand(_ args: [String], input: String? = nil) throws -> Comman
95180
}
96181

97182
process.waitUntilExit()
183+
group.wait()
98184

99-
let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
100-
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
185+
let stdout = String(decoding: stdoutBuffer.snapshot(), as: UTF8.self)
186+
let stderr = String(decoding: stderrBuffer.snapshot(), as: UTF8.self)
101187

102-
let stdout = String(decoding: stdoutData, as: UTF8.self)
103-
let stderr = String(decoding: stderrData, as: UTF8.self)
188+
debugLog { "Command exit status: \(process.terminationStatus)" }
189+
if !stdout.isEmpty {
190+
debugLog { "stdout (first 200 chars): \(stdout.prefix(200))" }
191+
}
192+
if !stderr.isEmpty {
193+
debugLog { "stderr (first 200 chars): \(stderr.prefix(200))" }
194+
}
104195

105196
if process.terminationStatus != 0 {
106197
let message = stderr.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -116,6 +207,7 @@ private func runAstGrep(configPath: String?, subcommand: String, args: [String],
116207
fullArgs += ["--config", configPath]
117208
}
118209
fullArgs += args
210+
debugLog { "ast-grep command args: \(fullArgs.joined(separator: " "))" }
119211
return try runCommand(fullArgs, input: input)
120212
}
121213

@@ -250,8 +342,10 @@ private func testMatchCodeRuleTool(_ args: [String: Value]?, configPath: String?
250342
throw MCPError.internalError("No matches found for the given code and rule. Try adding `stopBy: end` to inside/has rules.")
251343
}
252344

253-
let content = try jsonResourceContent(matches)
254-
return .init(content: [content], isError: false)
345+
// Note: Use plain text JSON here instead of jsonResourceContent to avoid
346+
// "TypeError: Cannot read properties of undefined (reading 'uri')" in some MCP clients.
347+
let jsonText = try encodeJSON(matches)
348+
return .init(content: [.text(jsonText)], isError: false)
255349
}
256350

257351
private func findCodeTool(_ args: [String: Value]?, configPath: String?) throws -> CallTool.Result {
@@ -405,11 +499,16 @@ private func buildTools(languages: [String]) -> [Tool] {
405499

406500
private func registerHandlers(server: Server, languages: [String], configPath: String?) async {
407501
await server.withMethodHandler(ListTools.self) { _ in
408-
.init(tools: buildTools(languages: languages))
502+
debugLog { "Handling list_tools" }
503+
return .init(tools: buildTools(languages: languages))
409504
}
410505

411506
await server.withMethodHandler(CallTool.self) { params in
412507
do {
508+
debugLog {
509+
let keys = params.arguments?.keys.joined(separator: ", ") ?? "<none>"
510+
return "Handling tool call: \(params.name) (args: \(keys))"
511+
}
413512
switch params.name {
414513
case "dump_syntax_tree":
415514
return try dumpSyntaxTreeTool(params.arguments, languages: languages, configPath: configPath)
@@ -439,24 +538,43 @@ struct AstGrepMCPServer: AsyncParsableCommand {
439538
discussion: "Environment: AST_GREP_CONFIG path to sgconfig.yaml (overridden by --config)"
440539
)
441540

541+
@Flag(name: .long, help: "Print verbose debug logs to stderr")
542+
var verbose = false
543+
544+
@Flag(name: [.short, .customLong("version")], help: "Print version information")
545+
var showVersion = false
546+
442547
@Option(name: .long, help: "Path to sgconfig.yaml file for customizing ast-grep behavior")
443548
var config: String?
444549

445550
mutating func run() async throws {
446-
let configPath = try resolveConfigPath(cliConfig: config)
447-
let languages = getSupportedLanguages(configPath: configPath)
448-
449-
let server = Server(
450-
name: "ast-grep",
451-
version: version,
452-
instructions: "Expose ast-grep CLI tools over MCP",
453-
capabilities: .init(tools: .init(listChanged: true))
454-
)
551+
if showVersion {
552+
print("ast-grep-mcp-swift \(version)")
553+
return
554+
}
455555

456-
await registerHandlers(server: server, languages: languages, configPath: configPath)
556+
try await DebugContext.$enabled.withValue(verbose) {
557+
if verbose {
558+
debugLog { "Verbose debug logging enabled" }
559+
}
560+
561+
let configPath = try resolveConfigPath(cliConfig: config)
562+
debugLog { "Using config path: \(configPath ?? "<none>")" }
563+
let languages = getSupportedLanguages(configPath: configPath)
564+
debugLog { "Loaded supported languages: \(languages.joined(separator: ", "))" }
565+
566+
let server = Server(
567+
name: "ast-grep",
568+
version: version,
569+
instructions: "Expose ast-grep CLI tools over MCP",
570+
capabilities: .init(tools: .init(listChanged: true))
571+
)
457572

458-
let transport = StdioTransport()
459-
try await server.start(transport: transport)
460-
await server.waitUntilCompleted()
573+
await registerHandlers(server: server, languages: languages, configPath: configPath)
574+
575+
let transport = StdioTransport()
576+
try await server.start(transport: transport)
577+
await server.waitUntilCompleted()
578+
}
461579
}
462580
}

0 commit comments

Comments
 (0)