@@ -4,6 +4,16 @@ import MCP
44import Yams
55
66private 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
818private 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+
69112private 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
257351private func findCodeTool( _ args: [ String : Value ] ? , configPath: String ? ) throws -> CallTool . Result {
@@ -405,11 +499,16 @@ private func buildTools(languages: [String]) -> [Tool] {
405499
406500private 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