-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_check_syntax.py
More file actions
180 lines (155 loc) · 5.79 KB
/
Copy pathtest_check_syntax.py
File metadata and controls
180 lines (155 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
"""
Small test client for the plantuml-mcp `check_syntax` tool over the
Streamable HTTP endpoint.
It performs the full MCP handshake and then calls `check_syntax` on a sample
diagram, printing the JSON result returned by the server.
Usage:
# Start the server in HTTP mode first:
# java -jar build/libs/plantuml-mcp-1.0.0.jar --spring.profiles.active=http
python test_check_syntax.py
python test_check_syntax.py --url http://localhost:8080/mcp
Only the standard library is used; no external dependencies.
"""
import argparse
import json
import sys
import urllib.error
import urllib.request
# MCP protocol version implemented by the server (see its initialize response).
PROTOCOL_VERSION = "2025-03-26"
# Both content types must be accepted: the server may answer with a plain JSON
# body or with a Server-Sent Events stream depending on the message.
ACCEPT = "application/json, text/event-stream"
# The diagram whose syntax we want to check.
SAMPLE_DIAGRAM = "\n".join([
"@startuml",
"skinparam handwritten true",
"Bob -> Alice : hello",
"@enduml",
])
def parse_body(raw: bytes, content_type: str) -> dict:
"""Parse a response body that is either raw JSON or an SSE stream.
For SSE, the JSON payload sits on one or more `data:` lines; we collect
them and parse the concatenated result.
"""
text = raw.decode("utf-8").strip()
if "text/event-stream" in content_type:
data_lines = []
for line in text.splitlines():
if line.startswith("data:"):
data_lines.append(line[len("data:"):].strip())
text = "".join(data_lines)
if text == "":
return {}
return json.loads(text)
def post(url: str, payload: dict, session_id: str | None) -> tuple[dict, str | None]:
"""Send one JSON-RPC message and return (parsed_body, session_id)."""
data = json.dumps(payload).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": ACCEPT,
}
if session_id is not None:
headers["Mcp-Session-Id"] = session_id
request = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(request) as response:
returned_session = response.headers.get("Mcp-Session-Id", session_id)
content_type = response.headers.get("Content-Type", "")
body = parse_body(response.read(), content_type)
return body, returned_session
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
raise SystemExit(f"HTTP {error.code} on {url}: {detail}") from error
except urllib.error.URLError as error:
raise SystemExit(
f"Cannot reach {url}: {error.reason}. "
"Is the server running with --spring.profiles.active=http ?"
) from error
def notify(url: str, payload: dict, session_id: str | None) -> None:
"""Send a JSON-RPC notification (no response expected)."""
data = json.dumps(payload).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": ACCEPT,
}
if session_id is not None:
headers["Mcp-Session-Id"] = session_id
request = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(request) as response:
response.read()
except urllib.error.HTTPError as error:
# Some servers answer 202 Accepted with an empty body for notifications.
if error.code not in (200, 202):
detail = error.read().decode("utf-8", errors="replace")
raise SystemExit(f"HTTP {error.code} on notify: {detail}") from error
def extract_text(tool_result: dict) -> str:
"""Pull the text payload out of a tools/call result."""
result = tool_result.get("result", {})
content = result.get("content", [])
texts = [item.get("text", "") for item in content if item.get("type") == "text"]
return "\n".join(texts).strip() or json.dumps(result)
def main() -> int:
parser = argparse.ArgumentParser(description="Test the plantuml-mcp check_syntax tool.")
parser.add_argument(
"--url",
default="http://localhost:8080/mcp",
help="MCP endpoint URL (default: http://localhost:8080/mcp)",
)
args = parser.parse_args()
# 1. initialize: open the session and negotiate the protocol version.
print(f"-> initialize ({args.url})")
init_payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "plantuml-mcp-tester", "version": "1.0"},
},
}
init_result, session_id = post(args.url, init_payload, None)
server_info = init_result.get("result", {}).get("serverInfo", {})
print(f" connected to {server_info.get('name', '?')} {server_info.get('version', '')}")
print(f" session id: {session_id}")
# 2. notifications/initialized: required by the protocol after initialize.
notify(
args.url,
{"jsonrpc": "2.0", "method": "notifications/initialized"},
session_id,
)
# 3. tools/call: invoke check_syntax with the sample diagram.
print("-> tools/call check_syntax")
print(" source:")
for line in SAMPLE_DIAGRAM.splitlines():
print(f" | {line}")
call_payload = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "check_syntax",
"arguments": {"source": SAMPLE_DIAGRAM},
},
}
call_result, session_id = post(args.url, call_payload, session_id)
raw = extract_text(call_result)
print("\nResult returned by the server:")
# The tool returns a CheckSyntaxRecord serialized as JSON, which Spring AI
# wraps as a text content item. Try to pretty-print it as JSON; fall back to
# the raw text if the shape differs.
try:
parsed = json.loads(raw)
print(json.dumps(parsed, indent=2))
if isinstance(parsed, dict) and parsed.get("valid") is True:
print(f"\n=> valid {parsed.get('diagramType')}")
elif isinstance(parsed, dict):
print(f"\n=> invalid: line {parsed.get('errorLine')}: {parsed.get('errorMessage')}")
except json.JSONDecodeError:
print(raw)
return 0
if __name__ == "__main__":
sys.exit(main())