Skip to content

Commit 74f8c8d

Browse files
committed
Add example stdio wrapper and config to be able to test the MCP server
1 parent 7e49da9 commit 74f8c8d

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

inst/examples/server_config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"mcpServers": {
3+
"plumber2mcp": {
4+
"command": "python3",
5+
"args": ["stdio-wrapper.py"],
6+
"cwd": "."
7+
}
8+
}
9+
}

inst/examples/stdio-wrapper.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Final production wrapper for plumber2mcp
4+
Handles all R jsonlite serialization quirks
5+
"""
6+
7+
import sys
8+
import json
9+
import urllib.request
10+
import urllib.error
11+
12+
MCP_ENDPOINT = "http://localhost:8000/mcp/messages"
13+
14+
def fix_r_json(obj):
15+
"""Fix all R jsonlite serialization issues"""
16+
if obj is None:
17+
return None
18+
elif isinstance(obj, list):
19+
if len(obj) == 1:
20+
# Convert single-element arrays to scalars
21+
return fix_r_json(obj[0])
22+
else:
23+
# Process array elements
24+
return [fix_r_json(item) for item in obj]
25+
elif isinstance(obj, dict):
26+
if len(obj) == 0:
27+
# Empty dict - context dependent
28+
return None # Most cases empty dict should be None
29+
30+
# Process dictionary recursively
31+
fixed = {}
32+
for k, v in obj.items():
33+
if k == "id" and isinstance(v, dict) and len(v) == 0:
34+
# Empty dict for id should be null
35+
fixed[k] = None
36+
elif k == "id" and isinstance(v, list) and len(v) == 1:
37+
# Single element array for id should be scalar
38+
fixed[k] = v[0]
39+
elif k == "tools" and isinstance(v, list) and len(v) == 0:
40+
# Empty tools array should be empty object for capabilities
41+
fixed[k] = {}
42+
elif k == "properties" and isinstance(v, list) and len(v) == 0:
43+
# Empty properties array should be empty object
44+
fixed[k] = {}
45+
elif k == "required" and isinstance(v, dict) and len(v) == 0:
46+
# Empty required dict should be empty array
47+
fixed[k] = []
48+
elif k == "content" and isinstance(v, list):
49+
# content is already an array, just fix nested arrays
50+
fixed[k] = [fix_r_json(item) for item in v]
51+
elif k == "error" and isinstance(v, dict):
52+
# Process error object specially
53+
error_fixed = {}
54+
for ek, ev in v.items():
55+
if isinstance(ev, list) and len(ev) == 1:
56+
error_fixed[ek] = ev[0]
57+
else:
58+
error_fixed[ek] = ev
59+
fixed[k] = error_fixed
60+
else:
61+
# Recursive fix
62+
fixed[k] = fix_r_json(v)
63+
return fixed
64+
else:
65+
return obj
66+
67+
def main():
68+
while True:
69+
try:
70+
line = sys.stdin.readline()
71+
if not line:
72+
break
73+
74+
request = json.loads(line.strip())
75+
76+
# Forward to HTTP endpoint
77+
data = json.dumps(request).encode('utf-8')
78+
req = urllib.request.Request(MCP_ENDPOINT,
79+
data=data,
80+
headers={'Content-Type': 'application/json'})
81+
82+
with urllib.request.urlopen(req) as response:
83+
result = json.loads(response.read().decode('utf-8'))
84+
85+
# Fix R serialization issues
86+
fixed_result = fix_r_json(result)
87+
88+
# Send response
89+
print(json.dumps(fixed_result))
90+
sys.stdout.flush()
91+
92+
except KeyboardInterrupt:
93+
break
94+
except Exception as e:
95+
# Send error response
96+
error_response = {
97+
"jsonrpc": "2.0",
98+
"id": request.get("id") if 'request' in locals() else None,
99+
"error": {
100+
"code": -32603,
101+
"message": f"Internal error: {str(e)}"
102+
}
103+
}
104+
print(json.dumps(error_response))
105+
sys.stdout.flush()
106+
107+
if __name__ == "__main__":
108+
main()

0 commit comments

Comments
 (0)