@ubuligan/playground 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/chunk-MASMRWUF.js +233 -0
- package/dist/chunk-MASMRWUF.js.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -0
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jsznpm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
|
|
5
|
+
// src/mcp-proxy.ts
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9
|
+
function parseTarget(raw) {
|
|
10
|
+
const trimmed = raw.trim();
|
|
11
|
+
if (/^https?:\/\//i.test(trimmed)) return { type: "http", url: trimmed };
|
|
12
|
+
const [command, ...args] = trimmed.split(/\s+/);
|
|
13
|
+
if (!command) throw new Error("Empty target");
|
|
14
|
+
return { type: "stdio", command, args };
|
|
15
|
+
}
|
|
16
|
+
function buildTransport(target) {
|
|
17
|
+
if (target.type === "http") {
|
|
18
|
+
return new StreamableHTTPClientTransport(new URL(target.url));
|
|
19
|
+
}
|
|
20
|
+
return new StdioClientTransport({
|
|
21
|
+
command: target.command,
|
|
22
|
+
args: target.args,
|
|
23
|
+
env: process.env
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
var McpProxy = class {
|
|
27
|
+
client = null;
|
|
28
|
+
target = null;
|
|
29
|
+
get connected() {
|
|
30
|
+
return this.client !== null;
|
|
31
|
+
}
|
|
32
|
+
async connect(target) {
|
|
33
|
+
await this.disconnect();
|
|
34
|
+
const client = new Client({ name: "mcp-playground", version: "0.1.0" });
|
|
35
|
+
await client.connect(buildTransport(target));
|
|
36
|
+
this.client = client;
|
|
37
|
+
this.target = target;
|
|
38
|
+
const info = client.getServerVersion();
|
|
39
|
+
return { name: info?.name ?? "mcp-server", version: info?.version ?? "0.0.0" };
|
|
40
|
+
}
|
|
41
|
+
async disconnect() {
|
|
42
|
+
if (this.client) {
|
|
43
|
+
await this.client.close().catch(() => {
|
|
44
|
+
});
|
|
45
|
+
this.client = null;
|
|
46
|
+
this.target = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
require() {
|
|
50
|
+
if (!this.client) throw new Error("Not connected. Connect to a server first.");
|
|
51
|
+
return this.client;
|
|
52
|
+
}
|
|
53
|
+
listTools() {
|
|
54
|
+
return this.require().listTools();
|
|
55
|
+
}
|
|
56
|
+
callTool(name, args) {
|
|
57
|
+
return this.require().callTool({ name, arguments: args });
|
|
58
|
+
}
|
|
59
|
+
listResources() {
|
|
60
|
+
return this.require().listResources();
|
|
61
|
+
}
|
|
62
|
+
readResource(uri) {
|
|
63
|
+
return this.require().readResource({ uri });
|
|
64
|
+
}
|
|
65
|
+
listPrompts() {
|
|
66
|
+
return this.require().listPrompts();
|
|
67
|
+
}
|
|
68
|
+
getPrompt(name, args) {
|
|
69
|
+
return this.require().getPrompt({ name, arguments: args });
|
|
70
|
+
}
|
|
71
|
+
status() {
|
|
72
|
+
return { connected: this.connected, target: this.target };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/ui.ts
|
|
77
|
+
var INDEX_HTML = (
|
|
78
|
+
/* html */
|
|
79
|
+
`<!doctype html>
|
|
80
|
+
<html lang="en">
|
|
81
|
+
<head>
|
|
82
|
+
<meta charset="utf-8" />
|
|
83
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
84
|
+
<title>MCP Playground</title>
|
|
85
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
86
|
+
<script type="importmap">
|
|
87
|
+
{
|
|
88
|
+
"imports": {
|
|
89
|
+
"preact": "https://esm.sh/preact@10.25.3",
|
|
90
|
+
"preact/hooks": "https://esm.sh/preact@10.25.3/hooks",
|
|
91
|
+
"htm": "https://esm.sh/htm@3.1.1"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
</head>
|
|
96
|
+
<body class="bg-slate-950 text-slate-100 min-h-screen">
|
|
97
|
+
<div id="app" class="max-w-5xl mx-auto p-6"></div>
|
|
98
|
+
<script type="module">
|
|
99
|
+
import { h, render } from "preact";
|
|
100
|
+
import { useState } from "preact/hooks";
|
|
101
|
+
import htm from "htm";
|
|
102
|
+
const html = htm.bind(h);
|
|
103
|
+
|
|
104
|
+
const api = async (path, body) => {
|
|
105
|
+
const res = await fetch("/api" + path, {
|
|
106
|
+
method: body ? "POST" : "GET",
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
109
|
+
});
|
|
110
|
+
const json = await res.json();
|
|
111
|
+
if (!res.ok) throw new Error(json.error || res.statusText);
|
|
112
|
+
return json;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function Field({ name, schema, value, onInput }) {
|
|
116
|
+
const type = schema?.type;
|
|
117
|
+
const label = html\`<label class="block text-sm text-slate-400 mb-1">\${name}\${schema?.description ? html\` \u2014 \${schema.description}\` : ""}</label>\`;
|
|
118
|
+
const cls = "w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm";
|
|
119
|
+
if (type === "boolean") {
|
|
120
|
+
return html\`<div class="mb-3">\${label}<input type="checkbox" checked=\${value} onChange=\${(e) => onInput(e.target.checked)} /></div>\`;
|
|
121
|
+
}
|
|
122
|
+
const inputType = type === "number" || type === "integer" ? "number" : "text";
|
|
123
|
+
return html\`<div class="mb-3">\${label}<input class=\${cls} type=\${inputType} value=\${value ?? ""} onInput=\${(e) => onInput(inputType === "number" ? Number(e.target.value) : e.target.value)} /></div>\`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ToolRunner({ tool }) {
|
|
127
|
+
const props = (tool.inputSchema && tool.inputSchema.properties) || {};
|
|
128
|
+
const [args, setArgs] = useState({});
|
|
129
|
+
const [result, setResult] = useState(null);
|
|
130
|
+
const [error, setError] = useState(null);
|
|
131
|
+
const [busy, setBusy] = useState(false);
|
|
132
|
+
|
|
133
|
+
const call = async () => {
|
|
134
|
+
setBusy(true); setError(null); setResult(null);
|
|
135
|
+
try {
|
|
136
|
+
const r = await api("/tools/call", { name: tool.name, args });
|
|
137
|
+
setResult(r);
|
|
138
|
+
} catch (e) { setError(e.message); }
|
|
139
|
+
setBusy(false);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return html\`
|
|
143
|
+
<div class="mt-4 p-4 bg-slate-900 rounded-lg border border-slate-800">
|
|
144
|
+
<h3 class="font-semibold text-cyan-300">\${tool.name}</h3>
|
|
145
|
+
<p class="text-sm text-slate-400 mb-3">\${tool.description || ""}</p>
|
|
146
|
+
\${Object.entries(props).map(([n, s]) => html\`<\${Field} name=\${n} schema=\${s} value=\${args[n]} onInput=\${(v) => setArgs({ ...args, [n]: v })} />\`)}
|
|
147
|
+
<button class="bg-cyan-600 hover:bg-cyan-500 px-4 py-2 rounded text-sm font-medium disabled:opacity-50" disabled=\${busy} onClick=\${call}>\${busy ? "Calling\u2026" : "Call tool"}</button>
|
|
148
|
+
\${error && html\`<pre class="mt-3 text-red-400 text-sm whitespace-pre-wrap">\${error}</pre>\`}
|
|
149
|
+
\${result && html\`<pre class="mt-3 bg-slate-950 p-3 rounded text-xs overflow-auto">\${JSON.stringify(result, null, 2)}</pre>\`}
|
|
150
|
+
</div>\`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function App() {
|
|
154
|
+
const [target, setTarget] = useState("");
|
|
155
|
+
const [server, setServer] = useState(null);
|
|
156
|
+
const [tools, setTools] = useState([]);
|
|
157
|
+
const [error, setError] = useState(null);
|
|
158
|
+
const [busy, setBusy] = useState(false);
|
|
159
|
+
|
|
160
|
+
const connect = async () => {
|
|
161
|
+
setBusy(true); setError(null);
|
|
162
|
+
try {
|
|
163
|
+
const info = await api("/connect", { target });
|
|
164
|
+
setServer(info);
|
|
165
|
+
const t = await api("/tools");
|
|
166
|
+
setTools(t.tools || []);
|
|
167
|
+
} catch (e) { setError(e.message); setServer(null); }
|
|
168
|
+
setBusy(false);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return html\`
|
|
172
|
+
<h1 class="text-2xl font-bold mb-1">MCP Playground</h1>
|
|
173
|
+
<p class="text-slate-400 text-sm mb-6">Connect to an MCP server and call its tools interactively.</p>
|
|
174
|
+
<div class="flex gap-2 mb-4">
|
|
175
|
+
<input class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm" placeholder='http://localhost:3000/mcp or "node dist/index.js"' value=\${target} onInput=\${(e) => setTarget(e.target.value)} />
|
|
176
|
+
<button class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-medium disabled:opacity-50" disabled=\${busy || !target} onClick=\${connect}>\${busy ? "\u2026" : "Connect"}</button>
|
|
177
|
+
</div>
|
|
178
|
+
\${error && html\`<div class="text-red-400 text-sm mb-4">\${error}</div>\`}
|
|
179
|
+
\${server && html\`<div class="text-emerald-400 text-sm mb-4">Connected to \${server.name} v\${server.version} \u2014 \${tools.length} tools</div>\`}
|
|
180
|
+
\${tools.map((t) => html\`<\${ToolRunner} tool=\${t} key=\${t.name} />\`)}\`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
render(h(App), document.getElementById("app"));
|
|
184
|
+
</script>
|
|
185
|
+
</body>
|
|
186
|
+
</html>`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// src/server.ts
|
|
190
|
+
function createApp(proxy = new McpProxy()) {
|
|
191
|
+
const app = new Hono();
|
|
192
|
+
app.get("/", (c) => c.html(INDEX_HTML));
|
|
193
|
+
app.post("/api/connect", async (c) => {
|
|
194
|
+
const { target } = await c.req.json();
|
|
195
|
+
const info = await proxy.connect(parseTarget(target));
|
|
196
|
+
return c.json(info);
|
|
197
|
+
});
|
|
198
|
+
app.post("/api/disconnect", async (c) => {
|
|
199
|
+
await proxy.disconnect();
|
|
200
|
+
return c.json({ ok: true });
|
|
201
|
+
});
|
|
202
|
+
app.get("/api/status", (c) => c.json(proxy.status()));
|
|
203
|
+
app.get("/api/tools", async (c) => c.json(await proxy.listTools()));
|
|
204
|
+
app.get("/api/resources", async (c) => c.json(await proxy.listResources()));
|
|
205
|
+
app.get("/api/prompts", async (c) => c.json(await proxy.listPrompts()));
|
|
206
|
+
app.post("/api/tools/call", async (c) => {
|
|
207
|
+
const { name, args } = await c.req.json();
|
|
208
|
+
return c.json(await proxy.callTool(name, args ?? {}));
|
|
209
|
+
});
|
|
210
|
+
app.post("/api/resources/read", async (c) => {
|
|
211
|
+
const { uri } = await c.req.json();
|
|
212
|
+
return c.json(await proxy.readResource(uri));
|
|
213
|
+
});
|
|
214
|
+
app.post("/api/prompts/get", async (c) => {
|
|
215
|
+
const { name, args } = await c.req.json();
|
|
216
|
+
return c.json(await proxy.getPrompt(name, args ?? {}));
|
|
217
|
+
});
|
|
218
|
+
app.onError((err, c) => c.json({ error: err instanceof Error ? err.message : String(err) }, 400));
|
|
219
|
+
return app;
|
|
220
|
+
}
|
|
221
|
+
function startPlayground(opts = {}) {
|
|
222
|
+
const port = opts.port ?? 4321;
|
|
223
|
+
const app = createApp();
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
serve({ fetch: app.fetch, port }, () => resolve(port));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
createApp,
|
|
231
|
+
startPlayground
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=chunk-MASMRWUF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/mcp-proxy.ts","../src/ui.ts"],"sourcesContent":["import { serve } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport { McpProxy, parseTarget } from \"./mcp-proxy.js\";\nimport { INDEX_HTML } from \"./ui.js\";\n\nexport function createApp(proxy: McpProxy = new McpProxy()): Hono {\n const app = new Hono();\n\n app.get(\"/\", (c) => c.html(INDEX_HTML));\n\n app.post(\"/api/connect\", async (c) => {\n const { target } = await c.req.json<{ target: string }>();\n const info = await proxy.connect(parseTarget(target));\n return c.json(info);\n });\n\n app.post(\"/api/disconnect\", async (c) => {\n await proxy.disconnect();\n return c.json({ ok: true });\n });\n\n app.get(\"/api/status\", (c) => c.json(proxy.status()));\n app.get(\"/api/tools\", async (c) => c.json(await proxy.listTools()));\n app.get(\"/api/resources\", async (c) => c.json(await proxy.listResources()));\n app.get(\"/api/prompts\", async (c) => c.json(await proxy.listPrompts()));\n\n app.post(\"/api/tools/call\", async (c) => {\n const { name, args } = await c.req.json<{ name: string; args?: Record<string, unknown> }>();\n return c.json(await proxy.callTool(name, args ?? {}));\n });\n\n app.post(\"/api/resources/read\", async (c) => {\n const { uri } = await c.req.json<{ uri: string }>();\n return c.json(await proxy.readResource(uri));\n });\n\n app.post(\"/api/prompts/get\", async (c) => {\n const { name, args } = await c.req.json<{ name: string; args?: Record<string, string> }>();\n return c.json(await proxy.getPrompt(name, args ?? {}));\n });\n\n // Turn proxy errors into JSON 400s instead of crashing the page.\n app.onError((err, c) => c.json({ error: err instanceof Error ? err.message : String(err) }, 400));\n\n return app;\n}\n\nexport interface StartOptions {\n port?: number;\n}\n\n/** Start the playground HTTP server. Resolves with the chosen port. */\nexport function startPlayground(opts: StartOptions = {}): Promise<number> {\n const port = opts.port ?? 4321;\n const app = createApp();\n return new Promise((resolve) => {\n serve({ fetch: app.fetch, port }, () => resolve(port));\n });\n}\n","import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\n\nexport type ProxyTarget =\n | { type: \"http\"; url: string }\n | { type: \"stdio\"; command: string; args?: string[] };\n\n/** Parse a UI-supplied target string into a structured target. */\nexport function parseTarget(raw: string): ProxyTarget {\n const trimmed = raw.trim();\n if (/^https?:\\/\\//i.test(trimmed)) return { type: \"http\", url: trimmed };\n const [command, ...args] = trimmed.split(/\\s+/);\n if (!command) throw new Error(\"Empty target\");\n return { type: \"stdio\", command, args };\n}\n\nfunction buildTransport(target: ProxyTarget): Transport {\n if (target.type === \"http\") {\n return new StreamableHTTPClientTransport(new URL(target.url));\n }\n return new StdioClientTransport({\n command: target.command,\n args: target.args,\n env: process.env as Record<string, string>,\n });\n}\n\n/**\n * Holds a single active MCP connection for the playground. The browser talks to\n * the local Hono server; this class does the actual MCP work server-side\n * (avoids CORS and lets stdio servers be spawned locally).\n */\nexport class McpProxy {\n private client: Client | null = null;\n private target: ProxyTarget | null = null;\n\n get connected(): boolean {\n return this.client !== null;\n }\n\n async connect(target: ProxyTarget): Promise<{ name: string; version: string }> {\n await this.disconnect();\n const client = new Client({ name: \"mcp-playground\", version: \"0.1.0\" });\n await client.connect(buildTransport(target));\n this.client = client;\n this.target = target;\n const info = client.getServerVersion();\n return { name: info?.name ?? \"mcp-server\", version: info?.version ?? \"0.0.0\" };\n }\n\n async disconnect(): Promise<void> {\n if (this.client) {\n await this.client.close().catch(() => {});\n this.client = null;\n this.target = null;\n }\n }\n\n private require(): Client {\n if (!this.client) throw new Error(\"Not connected. Connect to a server first.\");\n return this.client;\n }\n\n listTools() {\n return this.require().listTools();\n }\n callTool(name: string, args: Record<string, unknown>) {\n return this.require().callTool({ name, arguments: args });\n }\n listResources() {\n return this.require().listResources();\n }\n readResource(uri: string) {\n return this.require().readResource({ uri });\n }\n listPrompts() {\n return this.require().listPrompts();\n }\n getPrompt(name: string, args: Record<string, string>) {\n return this.require().getPrompt({ name, arguments: args });\n }\n\n status(): { connected: boolean; target: ProxyTarget | null } {\n return { connected: this.connected, target: this.target };\n }\n}\n","/**\n * The playground UI is a single self-contained HTML page. It loads Preact, htm\n * and Tailwind from a CDN (no build step) and talks to the local Hono API,\n * which proxies requests to the target MCP server.\n */\nexport const INDEX_HTML = /* html */ `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>MCP Playground</title>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <script type=\"importmap\">\n {\n \"imports\": {\n \"preact\": \"https://esm.sh/preact@10.25.3\",\n \"preact/hooks\": \"https://esm.sh/preact@10.25.3/hooks\",\n \"htm\": \"https://esm.sh/htm@3.1.1\"\n }\n }\n </script>\n </head>\n <body class=\"bg-slate-950 text-slate-100 min-h-screen\">\n <div id=\"app\" class=\"max-w-5xl mx-auto p-6\"></div>\n <script type=\"module\">\n import { h, render } from \"preact\";\n import { useState } from \"preact/hooks\";\n import htm from \"htm\";\n const html = htm.bind(h);\n\n const api = async (path, body) => {\n const res = await fetch(\"/api\" + path, {\n method: body ? \"POST\" : \"GET\",\n headers: { \"content-type\": \"application/json\" },\n body: body ? JSON.stringify(body) : undefined,\n });\n const json = await res.json();\n if (!res.ok) throw new Error(json.error || res.statusText);\n return json;\n };\n\n function Field({ name, schema, value, onInput }) {\n const type = schema?.type;\n const label = html\\`<label class=\"block text-sm text-slate-400 mb-1\">\\${name}\\${schema?.description ? html\\` — \\${schema.description}\\` : \"\"}</label>\\`;\n const cls = \"w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm\";\n if (type === \"boolean\") {\n return html\\`<div class=\"mb-3\">\\${label}<input type=\"checkbox\" checked=\\${value} onChange=\\${(e) => onInput(e.target.checked)} /></div>\\`;\n }\n const inputType = type === \"number\" || type === \"integer\" ? \"number\" : \"text\";\n return html\\`<div class=\"mb-3\">\\${label}<input class=\\${cls} type=\\${inputType} value=\\${value ?? \"\"} onInput=\\${(e) => onInput(inputType === \"number\" ? Number(e.target.value) : e.target.value)} /></div>\\`;\n }\n\n function ToolRunner({ tool }) {\n const props = (tool.inputSchema && tool.inputSchema.properties) || {};\n const [args, setArgs] = useState({});\n const [result, setResult] = useState(null);\n const [error, setError] = useState(null);\n const [busy, setBusy] = useState(false);\n\n const call = async () => {\n setBusy(true); setError(null); setResult(null);\n try {\n const r = await api(\"/tools/call\", { name: tool.name, args });\n setResult(r);\n } catch (e) { setError(e.message); }\n setBusy(false);\n };\n\n return html\\`\n <div class=\"mt-4 p-4 bg-slate-900 rounded-lg border border-slate-800\">\n <h3 class=\"font-semibold text-cyan-300\">\\${tool.name}</h3>\n <p class=\"text-sm text-slate-400 mb-3\">\\${tool.description || \"\"}</p>\n \\${Object.entries(props).map(([n, s]) => html\\`<\\${Field} name=\\${n} schema=\\${s} value=\\${args[n]} onInput=\\${(v) => setArgs({ ...args, [n]: v })} />\\`)}\n <button class=\"bg-cyan-600 hover:bg-cyan-500 px-4 py-2 rounded text-sm font-medium disabled:opacity-50\" disabled=\\${busy} onClick=\\${call}>\\${busy ? \"Calling…\" : \"Call tool\"}</button>\n \\${error && html\\`<pre class=\"mt-3 text-red-400 text-sm whitespace-pre-wrap\">\\${error}</pre>\\`}\n \\${result && html\\`<pre class=\"mt-3 bg-slate-950 p-3 rounded text-xs overflow-auto\">\\${JSON.stringify(result, null, 2)}</pre>\\`}\n </div>\\`;\n }\n\n function App() {\n const [target, setTarget] = useState(\"\");\n const [server, setServer] = useState(null);\n const [tools, setTools] = useState([]);\n const [error, setError] = useState(null);\n const [busy, setBusy] = useState(false);\n\n const connect = async () => {\n setBusy(true); setError(null);\n try {\n const info = await api(\"/connect\", { target });\n setServer(info);\n const t = await api(\"/tools\");\n setTools(t.tools || []);\n } catch (e) { setError(e.message); setServer(null); }\n setBusy(false);\n };\n\n return html\\`\n <h1 class=\"text-2xl font-bold mb-1\">MCP Playground</h1>\n <p class=\"text-slate-400 text-sm mb-6\">Connect to an MCP server and call its tools interactively.</p>\n <div class=\"flex gap-2 mb-4\">\n <input class=\"flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm\" placeholder='http://localhost:3000/mcp or \"node dist/index.js\"' value=\\${target} onInput=\\${(e) => setTarget(e.target.value)} />\n <button class=\"bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-medium disabled:opacity-50\" disabled=\\${busy || !target} onClick=\\${connect}>\\${busy ? \"…\" : \"Connect\"}</button>\n </div>\n \\${error && html\\`<div class=\"text-red-400 text-sm mb-4\">\\${error}</div>\\`}\n \\${server && html\\`<div class=\"text-emerald-400 text-sm mb-4\">Connected to \\${server.name} v\\${server.version} — \\${tools.length} tools</div>\\`}\n \\${tools.map((t) => html\\`<\\${ToolRunner} tool=\\${t} key=\\${t.name} />\\`)}\\`;\n }\n\n render(h(App), document.getElementById(\"app\"));\n </script>\n </body>\n</html>`;\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
startPlayground
|
|
4
|
+
} from "./chunk-MASMRWUF.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
var program = new Command();
|
|
10
|
+
program.name("mcp-playground").description("Launch the local MCP playground web UI").option("-p, --port <number>", "Port to listen on", "4321").action(async (opts) => {
|
|
11
|
+
const port = await startPlayground({ port: Number(opts.port) });
|
|
12
|
+
process.stdout.write(pc.green(`
|
|
13
|
+
MCP Playground \u2192 ${pc.bold(`http://localhost:${port}`)}
|
|
14
|
+
|
|
15
|
+
`));
|
|
16
|
+
});
|
|
17
|
+
program.parseAsync();
|
|
18
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { startPlayground } from \"./server.js\";\n\nconst program = new Command();\nprogram\n .name(\"mcp-playground\")\n .description(\"Launch the local MCP playground web UI\")\n .option(\"-p, --port <number>\", \"Port to listen on\", \"4321\")\n .action(async (opts: { port: string }) => {\n const port = await startPlayground({ port: Number(opts.port) });\n process.stdout.write(pc.green(`\\n MCP Playground → ${pc.bold(`http://localhost:${port}`)}\\n\\n`));\n });\n\nprogram.parseAsync();\n"],"mappings":";;;;;;AACA,SAAS,eAAe;AACxB,OAAO,QAAQ;AAGf,IAAM,UAAU,IAAI,QAAQ;AAC5B,QACG,KAAK,gBAAgB,EACrB,YAAY,wCAAwC,EACpD,OAAO,uBAAuB,qBAAqB,MAAM,EACzD,OAAO,OAAO,SAA2B;AACxC,QAAM,OAAO,MAAM,gBAAgB,EAAE,MAAM,OAAO,KAAK,IAAI,EAAE,CAAC;AAC9D,UAAQ,OAAO,MAAM,GAAG,MAAM;AAAA,0BAAwB,GAAG,KAAK,oBAAoB,IAAI,EAAE,CAAC;AAAA;AAAA,CAAM,CAAC;AAClG,CAAC;AAEH,QAAQ,WAAW;","names":[]}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ubuligan/playground",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local web playground to interactively explore and call MCP servers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-playground": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@hono/node-server": "^1.13.7",
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
17
|
+
"commander": "^12.1.0",
|
|
18
|
+
"hono": "^4.6.14",
|
|
19
|
+
"picocolors": "^1.1.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsx": "^4.19.2",
|
|
23
|
+
"tsup": "^8.3.5",
|
|
24
|
+
"typescript": "^5.7.2"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"author": "jsznpm",
|
|
30
|
+
"homepage": "https://github.com/jsznpm/create-mcp-toolkit#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/jsznpm/create-mcp-toolkit/issues"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/jsznpm/create-mcp-toolkit.git",
|
|
37
|
+
"directory": "packages/playground"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"mcp",
|
|
41
|
+
"model-context-protocol",
|
|
42
|
+
"ai",
|
|
43
|
+
"llm",
|
|
44
|
+
"playground",
|
|
45
|
+
"ui",
|
|
46
|
+
"devtools"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"dev": "tsx src/cli.ts"
|
|
52
|
+
}
|
|
53
|
+
}
|