Skip to content

Commit d9cbcf3

Browse files
committed
fix: add ignorable input boolean for introspect schema
Another workaround for clients that send undefined as an argument when inputArguments is an empty object Also added two debug clients to fool around with this a bit
1 parent dfbee45 commit d9cbcf3

File tree

4 files changed

+231
-2
lines changed

4 files changed

+231
-2
lines changed

dev/debug-client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Small debug client to test a few specific interactions
2+
3+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5+
6+
const transport = new StdioClientTransport({
7+
command: "node",
8+
args: ["dist/index.js"],
9+
});
10+
11+
const client = new Client({
12+
name: "debug-client",
13+
version: "1.0.0",
14+
});
15+
16+
await client.connect(transport);
17+
18+
// Call introspect-schema with undefined argument
19+
const result = await client.callTool({
20+
name: "introspect-schema",
21+
arguments: {},
22+
});
23+
24+
console.log(result);

dev/debug-manual-client.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Manual MCP client using stdio directly (no SDK)
2+
// This demonstrates the raw JSON-RPC protocol communication
3+
4+
import { type ChildProcess, spawn } from "node:child_process";
5+
import { createInterface } from "node:readline";
6+
7+
interface JsonRpcMessage {
8+
jsonrpc: "2.0";
9+
id?: string | number;
10+
method?: string;
11+
params?: unknown;
12+
result?: unknown;
13+
error?: {
14+
code: number;
15+
message: string;
16+
data?: unknown;
17+
};
18+
}
19+
20+
class ManualMcpClient {
21+
private serverProcess: ChildProcess;
22+
private messageId = 1;
23+
private pendingRequests = new Map<
24+
string | number,
25+
(response: JsonRpcMessage) => void
26+
>();
27+
28+
constructor() {
29+
// Start the MCP server process
30+
this.serverProcess = spawn("node", ["dist/index.js"], {
31+
stdio: ["pipe", "pipe", "pipe"],
32+
});
33+
34+
// Set up readline to read server responses line by line
35+
if (this.serverProcess.stdout) {
36+
const rl = createInterface({
37+
input: this.serverProcess.stdout,
38+
});
39+
40+
rl.on("line", (line) => {
41+
try {
42+
const message: JsonRpcMessage = JSON.parse(line);
43+
this.handleServerMessage(message);
44+
} catch (error) {
45+
console.error("Failed to parse server message:", line, error);
46+
}
47+
});
48+
}
49+
50+
// Handle server errors
51+
this.serverProcess.stderr?.on("data", (data: Buffer) => {
52+
console.error("Server stderr:", data.toString());
53+
});
54+
55+
this.serverProcess.on("exit", (code: number | null) => {
56+
console.log(`Server process exited with code ${code}`);
57+
});
58+
}
59+
60+
private handleServerMessage(message: JsonRpcMessage) {
61+
console.log("← Received from server:", JSON.stringify(message, null, 2));
62+
63+
// Handle responses to our requests
64+
if (message.id !== undefined && this.pendingRequests.has(message.id)) {
65+
const resolver = this.pendingRequests.get(message.id);
66+
if (resolver) {
67+
this.pendingRequests.delete(message.id);
68+
resolver(message);
69+
}
70+
}
71+
}
72+
73+
private sendMessage(message: JsonRpcMessage): Promise<JsonRpcMessage> {
74+
const messageStr = JSON.stringify(message);
75+
console.log("→ Sending to server:", messageStr);
76+
77+
this.serverProcess.stdin?.write(`${messageStr}\n`);
78+
79+
// If this is a request (has an id), wait for response
80+
if (message.id !== undefined) {
81+
return new Promise((resolve) => {
82+
if (message.id !== undefined) {
83+
this.pendingRequests.set(message.id, resolve);
84+
}
85+
});
86+
}
87+
88+
return Promise.resolve(message);
89+
}
90+
91+
private getNextId(): number {
92+
return this.messageId++;
93+
}
94+
95+
async initialize(): Promise<JsonRpcMessage> {
96+
const initMessage: JsonRpcMessage = {
97+
jsonrpc: "2.0",
98+
method: "initialize",
99+
params: {
100+
protocolVersion: "2025-03-26",
101+
capabilities: {},
102+
clientInfo: {
103+
name: "manual-debug-client",
104+
version: "1.0.0",
105+
},
106+
},
107+
id: this.getNextId(),
108+
};
109+
110+
const response = await this.sendMessage(initMessage);
111+
112+
// Send initialized notification
113+
const initializedNotification: JsonRpcMessage = {
114+
jsonrpc: "2.0",
115+
method: "notifications/initialized",
116+
};
117+
118+
await this.sendMessage(initializedNotification);
119+
120+
return response;
121+
}
122+
123+
async ping(): Promise<JsonRpcMessage> {
124+
const pingMessage: JsonRpcMessage = {
125+
jsonrpc: "2.0",
126+
method: "ping",
127+
id: this.getNextId(),
128+
};
129+
130+
return this.sendMessage(pingMessage);
131+
}
132+
133+
async introspectSchema(): Promise<JsonRpcMessage> {
134+
const introspectMessage: JsonRpcMessage = {
135+
jsonrpc: "2.0",
136+
method: "tools/call",
137+
params: {
138+
name: "introspect-schema",
139+
arguments: {},
140+
},
141+
id: this.getNextId(),
142+
};
143+
144+
return this.sendMessage(introspectMessage);
145+
}
146+
147+
async listTools(): Promise<JsonRpcMessage> {
148+
const listToolsMessage: JsonRpcMessage = {
149+
jsonrpc: "2.0",
150+
method: "tools/list",
151+
params: {},
152+
id: this.getNextId(),
153+
};
154+
155+
return this.sendMessage(listToolsMessage);
156+
}
157+
158+
async close() {
159+
this.serverProcess.kill();
160+
}
161+
}
162+
163+
// Main execution
164+
async function main() {
165+
console.log("🚀 Starting manual MCP client...");
166+
167+
const client = new ManualMcpClient();
168+
169+
try {
170+
// Wait a bit for the server to start
171+
await new Promise((resolve) => setTimeout(resolve, 1000));
172+
173+
console.log("\n📋 Step 1: Initialize connection");
174+
const initResponse = await client.initialize();
175+
console.log("✅ Initialization complete");
176+
177+
console.log("\n📋 Step 2: Ping server");
178+
const pingResponse = await client.ping();
179+
console.log("✅ Ping successful");
180+
181+
console.log("\n📋 Step 3: List available tools");
182+
const toolsResponse = await client.listTools();
183+
console.log("✅ Tools listed");
184+
185+
console.log("\n📋 Step 4: Call introspect-schema tool");
186+
const schemaResponse = await client.introspectSchema();
187+
console.log("✅ Schema introspection complete");
188+
189+
console.log("\n🎉 All operations completed successfully!");
190+
} catch (error) {
191+
console.error("❌ Error:", error);
192+
} finally {
193+
console.log("\n🔚 Closing client...");
194+
client.close();
195+
}
196+
}
197+
198+
main().catch(console.error);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "mcp-graphql",
33
"module": "index.ts",
44
"type": "module",
5-
"version": "2.0.3",
5+
"version": "2.0.4",
66
"repository": "github:blurrah/mcp-graphql",
77
"license": "MIT",
88
"bin": {

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,14 @@ server.resource("graphql-schema", new URL(env.ENDPOINT).href, async (uri) => {
6767
server.tool(
6868
"introspect-schema",
6969
"Introspect the GraphQL schema, use this tool before doing a query to get the schema information if you do not have it available as a resource already.",
70-
{},
70+
{
71+
// This is a workaround to help clients that can't handle an empty object as an argument
72+
// They will often send undefined instead of an empty object which is not allowed by the schema
73+
__ignore__: z
74+
.boolean()
75+
.default(false)
76+
.describe("This does not do anything"),
77+
},
7178
async () => {
7279
try {
7380
let schema: string;

0 commit comments

Comments
 (0)