mcp-playbook 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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/dev-server.ts","../../src/server/connector.ts"],"sourcesContent":["/**\n * src/server/dev-server.ts\n *\n * The development server.\n * Serves the React UI + REST API + WebSocket hot reload.\n */\n\nimport express from 'express'\nimport { createServer } from 'http'\nimport { WebSocketServer } from 'ws'\nimport path from 'path'\nimport fs from 'fs'\nimport chalk from 'chalk'\nimport chokidar from 'chokidar'\nimport open from 'open'\nimport { discoverAllServers, executeTool } from './connector.js'\nimport type { PlaybookConfig } from '../config.js'\n\ninterface DevServerOptions {\n port: number\n configPath: string\n open: boolean\n}\n\n// ── Resolve client dist path robustly ───────────────────────────────\n// Works both when run from source (via ts-node) and from compiled dist/\nfunction resolveClientDist(): string {\n // __dirname in CJS = directory of the compiled file\n // When compiled: dist/server/dev-server.js → dist/client/\n const fromCompiled = path.join(__dirname, '../client')\n if (fs.existsSync(fromCompiled)) return fromCompiled\n\n // Fallback: look relative to package root\n const fromRoot = path.join(__dirname, '../../dist/client')\n if (fs.existsSync(fromRoot)) return fromRoot\n\n return fromCompiled // return anyway — will be checked later\n}\n\n// ── Minimal fallback HTML when client is not built ───────────────────\nfunction fallbackHTML(port: number): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\"/>\n <title>MCP Playbook</title>\n <style>\n body { font-family: monospace; background: #080808; color: #e2e0d8;\n display: flex; align-items: center; justify-content: center;\n height: 100vh; margin: 0; flex-direction: column; gap: 12px; }\n code { background: #1a1a2e; color: #a39cf0; padding: 4px 10px;\n border-radius: 4px; font-size: 14px; }\n p { color: #5a5856; font-size: 13px; }\n </style>\n</head>\n<body>\n <div style=\"font-size:32px\">⚡</div>\n <h2 style=\"margin:0\">MCP Playbook API is running</h2>\n <p>The React UI bundle was not found at <code>dist/client/</code></p>\n <p>Run <code>npm run build:client</code> inside the mcp-playbook library to build it.</p>\n <p>API available at <code>http://localhost:${port}/api/servers</code></p>\n</body>\n</html>`\n}\n\nexport async function startDevServer(options: DevServerOptions) {\n const app = express()\n const server = createServer(app)\n const wss = new WebSocketServer({ server })\n\n app.use(express.json())\n\n // ── Serve static client bundle ──────────────────────────────────\n const clientDist = resolveClientDist()\n const hasClient = fs.existsSync(path.join(clientDist, 'index.html'))\n\n if (hasClient) {\n app.use(express.static(clientDist))\n console.log(chalk.dim(` Client: serving from ${clientDist}`))\n } else {\n console.log(chalk.yellow(` Warning: client bundle not found at ${clientDist}`))\n console.log(chalk.dim(' API is available but UI will show a placeholder'))\n }\n\n // ── Config loader ───────────────────────────────────────────────\n async function loadConfig(): Promise<PlaybookConfig> {\n // Clear module cache for hot reload\n const key = require.resolve(options.configPath)\n delete require.cache[key]\n\n // Try TypeScript config via ts-node/register if available\n try {\n return require(options.configPath).default\n } catch {\n // ts-node not available — try pre-compiled JS version\n const jsPath = options.configPath.replace(/\\.ts$/, '.js')\n if (fs.existsSync(jsPath)) {\n delete require.cache[require.resolve(jsPath)]\n return require(jsPath).default\n }\n throw new Error(\n `Cannot load config: ${options.configPath}\\n` +\n ` Make sure ts-node is installed: npm install -D ts-node\\n` +\n ` Or compile your config to JS first.`\n )\n }\n }\n\n // ── Discover servers ────────────────────────────────────────────\n let cachedServers: Awaited<ReturnType<typeof discoverAllServers>> = []\n let cachedConfig: PlaybookConfig | null = null\n\n async function refreshServers() {\n try {\n const config = await loadConfig()\n cachedConfig = config\n cachedServers = await discoverAllServers(\n config.servers,\n config.examples || {},\n config.tags || {}\n )\n const connected = cachedServers.filter(s => s.status === 'connected').length\n const tools = cachedServers.reduce((acc, s) => acc + s.tools.length, 0)\n console.log(chalk.green(` ✓ ${connected}/${cachedServers.length} servers · ${tools} tools`))\n broadcastToClients({ type: 'reload' })\n } catch (err: any) {\n console.error(chalk.red(` ✗ ${err.message}`))\n }\n }\n\n function broadcastToClients(data: unknown) {\n const msg = JSON.stringify(data)\n wss.clients.forEach(c => { if (c.readyState === 1) c.send(msg) })\n }\n\n // ── REST API ────────────────────────────────────────────────────\n\n app.get('/api/servers', async (_req, res) => {\n try {\n res.json({\n title: cachedConfig?.title || 'MCP Playbook',\n description: cachedConfig?.description || '',\n theme: cachedConfig?.theme || {},\n servers: cachedServers\n })\n } catch (err: any) {\n res.status(500).json({ error: err.message })\n }\n })\n\n app.post('/api/refresh', async (_req, res) => {\n await refreshServers()\n res.json({ ok: true })\n })\n\n app.post('/api/execute', async (req, res) => {\n const { serverId, toolName, input } = req.body\n if (!cachedConfig) return res.status(503).json({ error: 'Config not loaded' })\n\n const idx = parseInt(serverId.replace('server-', ''))\n const serverConfig = cachedConfig.servers[idx]\n if (!serverConfig) return res.status(404).json({ error: `Server not found: ${serverId}` })\n\n const result = await executeTool(serverConfig, toolName, input || {})\n res.json(result)\n })\n\n // Fallback — serve index.html for SPA routing\n app.get('*', (_req, res) => {\n const indexPath = path.join(clientDist, 'index.html')\n if (fs.existsSync(indexPath)) {\n res.sendFile(indexPath)\n } else {\n res.send(fallbackHTML(options.port))\n }\n })\n\n // ── WebSocket ───────────────────────────────────────────────────\n wss.on('connection', (ws) => {\n ws.send(JSON.stringify({ type: 'connected' }))\n })\n\n // ── Config watcher ──────────────────────────────────────────────\n const watcher = chokidar.watch(options.configPath, { ignoreInitial: true })\n watcher.on('change', () => {\n console.log(chalk.dim('\\n Config changed — refreshing...'))\n refreshServers()\n })\n\n // ── Start ───────────────────────────────────────────────────────\n await new Promise<void>((resolve, reject) => {\n server.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(new Error(`Port ${options.port} is already in use. Try: --port ${options.port + 1}`))\n } else {\n reject(err)\n }\n })\n server.listen(options.port, () => resolve())\n })\n\n await refreshServers()\n\n const url = `http://localhost:${options.port}`\n console.log(chalk.dim(`\\n Local: `) + chalk.cyan(url))\n console.log(chalk.dim(` Config: ${options.configPath}\\n`))\n\n if (options.open && hasClient) {\n try { await open(url) } catch {}\n }\n\n return server\n}\n","/**\n * src/server/connector.ts\n *\n * Connects to MCP servers and discovers their tools.\n * This is the bridge between the user's MCP servers and\n * the Playbook UI.\n *\n * Supports:\n * - stdio transport (spawn a process, talk over stdin/stdout)\n * - HTTP / SSE transport (connect to a running server)\n *\n * Returns a unified DiscoveredServer[] with all tool schemas.\n */\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { ServerConfig, ToolExample } from '../config.js'\n\nexport interface DiscoveredTool {\n name: string\n description: string\n inputSchema: Record<string, unknown>\n examples: ToolExample[]\n tags: string[]\n}\n\nexport interface DiscoveredServer {\n id: string\n name: string\n url: string\n status: 'connected' | 'disconnected' | 'error'\n version: string\n tools: DiscoveredTool[]\n error?: string\n}\n\n/**\n * Connect to all configured servers and discover their tools.\n * Runs all connections in parallel — slow servers don't block fast ones.\n */\nexport async function discoverAllServers(\n servers: ServerConfig[],\n extraExamples: Record<string, ToolExample[]> = {},\n tags: Record<string, string[]> = {}\n): Promise<DiscoveredServer[]> {\n\n // Build a reverse lookup: tool name → tag names\n const toolTagMap: Record<string, string[]> = {}\n for (const [tag, tools] of Object.entries(tags)) {\n for (const tool of tools) {\n if (!toolTagMap[tool]) toolTagMap[tool] = []\n toolTagMap[tool].push(tag)\n }\n }\n\n // Connect to all servers in parallel\n const results = await Promise.allSettled(\n servers.map((server, i) =>\n connectToServer(server, i, extraExamples, toolTagMap)\n )\n )\n\n return results.map((result, i) => {\n if (result.status === 'fulfilled') {\n return result.value\n } else {\n return {\n id: `server-${i}`,\n name: servers[i].name,\n url: servers[i].url || `${servers[i].command} ${(servers[i].args || []).join(' ')}`,\n status: 'error' as const,\n version: 'unknown',\n tools: [],\n error: result.reason?.message || 'Connection failed'\n }\n }\n })\n}\n\nasync function connectToServer(\n config: ServerConfig,\n index: number,\n extraExamples: Record<string, ToolExample[]>,\n toolTagMap: Record<string, string[]>\n): Promise<DiscoveredServer> {\n\n const client = new Client(\n { name: 'mcp-playbook', version: '0.1.0' },\n { capabilities: {} }\n )\n\n // Create the right transport based on config\n let transport\n\n if (config.transport === 'stdio') {\n if (!config.command) throw new Error(`Server \"${config.name}\" needs a command for stdio transport`)\n transport = new StdioClientTransport({\n command: config.command,\n args: config.args || [],\n env: config.env || {},\n cwd: config.cwd || process.cwd()\n })\n } else if (config.transport === 'http') {\n if (!config.url) throw new Error(`Server \"${config.name}\" needs a url for http transport`)\n transport = new StreamableHTTPClientTransport(new URL(config.url), {\n requestInit: { headers: config.headers || {} }\n })\n } else if (config.transport === 'sse') {\n if (!config.url) throw new Error(`Server \"${config.name}\" needs a url for sse transport`)\n transport = new SSEClientTransport(new URL(config.url), {\n requestInit: { headers: config.headers || {} }\n })\n } else {\n throw new Error(`Unknown transport: ${(config as any).transport}`)\n }\n\n await client.connect(transport)\n\n // Fetch tools from the server\n const { tools } = await client.listTools()\n\n // Fetch server info if available\n const serverInfo = client.getServerVersion()\n\n // Map tools to our format\n const discoveredTools: DiscoveredTool[] = tools.map(tool => ({\n name: tool.name,\n description: tool.description || '',\n inputSchema: tool.inputSchema as Record<string, unknown>,\n examples: extraExamples[tool.name] || [],\n tags: toolTagMap[tool.name] || []\n }))\n\n return {\n id: `server-${index}`,\n name: config.name,\n url: config.url || `${config.command} ${(config.args || []).join(' ')}`,\n status: 'connected',\n version: serverInfo?.version || '1.0.0',\n tools: discoveredTools\n }\n}\n\n/**\n * Execute a tool call against a specific server.\n * Used by the Try tab in the UI.\n */\nexport async function executeTool(\n serverConfig: ServerConfig,\n toolName: string,\n input: Record<string, unknown>\n): Promise<{ result: unknown; duration_ms: number; error?: string }> {\n\n const start = Date.now()\n\n try {\n const client = new Client(\n { name: 'mcp-playbook', version: '0.1.0' },\n { capabilities: {} }\n )\n\n let transport\n if (serverConfig.transport === 'stdio') {\n transport = new StdioClientTransport({\n command: serverConfig.command!,\n args: serverConfig.args || [],\n })\n } else {\n transport = new StreamableHTTPClientTransport(new URL(serverConfig.url!))\n }\n\n await client.connect(transport)\n\n const result = await client.callTool({ name: toolName, arguments: input })\n await client.close()\n\n return {\n result,\n duration_ms: Date.now() - start\n }\n } catch (err: any) {\n return {\n result: null,\n duration_ms: Date.now() - start,\n error: err.message\n }\n }\n}\n"],"mappings":"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","names":["path","fs","express","chalk","chokidar","open"]}
@@ -0,0 +1,287 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/server/dev-server.ts
9
+ import express from "express";
10
+ import { createServer } from "http";
11
+ import { WebSocketServer } from "ws";
12
+ import path from "path";
13
+ import fs from "fs";
14
+ import chalk from "chalk";
15
+ import chokidar from "chokidar";
16
+ import open from "open";
17
+
18
+ // src/server/connector.ts
19
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
20
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
21
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
22
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
23
+ async function discoverAllServers(servers, extraExamples = {}, tags = {}) {
24
+ const toolTagMap = {};
25
+ for (const [tag, tools] of Object.entries(tags)) {
26
+ for (const tool of tools) {
27
+ if (!toolTagMap[tool]) toolTagMap[tool] = [];
28
+ toolTagMap[tool].push(tag);
29
+ }
30
+ }
31
+ const results = await Promise.allSettled(
32
+ servers.map(
33
+ (server, i) => connectToServer(server, i, extraExamples, toolTagMap)
34
+ )
35
+ );
36
+ return results.map((result, i) => {
37
+ if (result.status === "fulfilled") {
38
+ return result.value;
39
+ } else {
40
+ return {
41
+ id: `server-${i}`,
42
+ name: servers[i].name,
43
+ url: servers[i].url || `${servers[i].command} ${(servers[i].args || []).join(" ")}`,
44
+ status: "error",
45
+ version: "unknown",
46
+ tools: [],
47
+ error: result.reason?.message || "Connection failed"
48
+ };
49
+ }
50
+ });
51
+ }
52
+ async function connectToServer(config, index, extraExamples, toolTagMap) {
53
+ const client = new Client(
54
+ { name: "mcp-playbook", version: "0.1.0" },
55
+ { capabilities: {} }
56
+ );
57
+ let transport;
58
+ if (config.transport === "stdio") {
59
+ if (!config.command) throw new Error(`Server "${config.name}" needs a command for stdio transport`);
60
+ transport = new StdioClientTransport({
61
+ command: config.command,
62
+ args: config.args || [],
63
+ env: config.env || {},
64
+ cwd: config.cwd || process.cwd()
65
+ });
66
+ } else if (config.transport === "http") {
67
+ if (!config.url) throw new Error(`Server "${config.name}" needs a url for http transport`);
68
+ transport = new StreamableHTTPClientTransport(new URL(config.url), {
69
+ requestInit: { headers: config.headers || {} }
70
+ });
71
+ } else if (config.transport === "sse") {
72
+ if (!config.url) throw new Error(`Server "${config.name}" needs a url for sse transport`);
73
+ transport = new SSEClientTransport(new URL(config.url), {
74
+ requestInit: { headers: config.headers || {} }
75
+ });
76
+ } else {
77
+ throw new Error(`Unknown transport: ${config.transport}`);
78
+ }
79
+ await client.connect(transport);
80
+ const { tools } = await client.listTools();
81
+ const serverInfo = client.getServerVersion();
82
+ const discoveredTools = tools.map((tool) => ({
83
+ name: tool.name,
84
+ description: tool.description || "",
85
+ inputSchema: tool.inputSchema,
86
+ examples: extraExamples[tool.name] || [],
87
+ tags: toolTagMap[tool.name] || []
88
+ }));
89
+ return {
90
+ id: `server-${index}`,
91
+ name: config.name,
92
+ url: config.url || `${config.command} ${(config.args || []).join(" ")}`,
93
+ status: "connected",
94
+ version: serverInfo?.version || "1.0.0",
95
+ tools: discoveredTools
96
+ };
97
+ }
98
+ async function executeTool(serverConfig, toolName, input) {
99
+ const start = Date.now();
100
+ try {
101
+ const client = new Client(
102
+ { name: "mcp-playbook", version: "0.1.0" },
103
+ { capabilities: {} }
104
+ );
105
+ let transport;
106
+ if (serverConfig.transport === "stdio") {
107
+ transport = new StdioClientTransport({
108
+ command: serverConfig.command,
109
+ args: serverConfig.args || []
110
+ });
111
+ } else {
112
+ transport = new StreamableHTTPClientTransport(new URL(serverConfig.url));
113
+ }
114
+ await client.connect(transport);
115
+ const result = await client.callTool({ name: toolName, arguments: input });
116
+ await client.close();
117
+ return {
118
+ result,
119
+ duration_ms: Date.now() - start
120
+ };
121
+ } catch (err) {
122
+ return {
123
+ result: null,
124
+ duration_ms: Date.now() - start,
125
+ error: err.message
126
+ };
127
+ }
128
+ }
129
+
130
+ // src/server/dev-server.ts
131
+ function resolveClientDist() {
132
+ const fromCompiled = path.join(__dirname, "../client");
133
+ if (fs.existsSync(fromCompiled)) return fromCompiled;
134
+ const fromRoot = path.join(__dirname, "../../dist/client");
135
+ if (fs.existsSync(fromRoot)) return fromRoot;
136
+ return fromCompiled;
137
+ }
138
+ function fallbackHTML(port) {
139
+ return `<!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <meta charset="UTF-8"/>
143
+ <title>MCP Playbook</title>
144
+ <style>
145
+ body { font-family: monospace; background: #080808; color: #e2e0d8;
146
+ display: flex; align-items: center; justify-content: center;
147
+ height: 100vh; margin: 0; flex-direction: column; gap: 12px; }
148
+ code { background: #1a1a2e; color: #a39cf0; padding: 4px 10px;
149
+ border-radius: 4px; font-size: 14px; }
150
+ p { color: #5a5856; font-size: 13px; }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div style="font-size:32px">\u26A1</div>
155
+ <h2 style="margin:0">MCP Playbook API is running</h2>
156
+ <p>The React UI bundle was not found at <code>dist/client/</code></p>
157
+ <p>Run <code>npm run build:client</code> inside the mcp-playbook library to build it.</p>
158
+ <p>API available at <code>http://localhost:${port}/api/servers</code></p>
159
+ </body>
160
+ </html>`;
161
+ }
162
+ async function startDevServer(options) {
163
+ const app = express();
164
+ const server = createServer(app);
165
+ const wss = new WebSocketServer({ server });
166
+ app.use(express.json());
167
+ const clientDist = resolveClientDist();
168
+ const hasClient = fs.existsSync(path.join(clientDist, "index.html"));
169
+ if (hasClient) {
170
+ app.use(express.static(clientDist));
171
+ console.log(chalk.dim(` Client: serving from ${clientDist}`));
172
+ } else {
173
+ console.log(chalk.yellow(` Warning: client bundle not found at ${clientDist}`));
174
+ console.log(chalk.dim(" API is available but UI will show a placeholder"));
175
+ }
176
+ async function loadConfig() {
177
+ const key = __require.resolve(options.configPath);
178
+ delete __require.cache[key];
179
+ try {
180
+ return __require(options.configPath).default;
181
+ } catch {
182
+ const jsPath = options.configPath.replace(/\.ts$/, ".js");
183
+ if (fs.existsSync(jsPath)) {
184
+ delete __require.cache[__require.resolve(jsPath)];
185
+ return __require(jsPath).default;
186
+ }
187
+ throw new Error(
188
+ `Cannot load config: ${options.configPath}
189
+ Make sure ts-node is installed: npm install -D ts-node
190
+ Or compile your config to JS first.`
191
+ );
192
+ }
193
+ }
194
+ let cachedServers = [];
195
+ let cachedConfig = null;
196
+ async function refreshServers() {
197
+ try {
198
+ const config = await loadConfig();
199
+ cachedConfig = config;
200
+ cachedServers = await discoverAllServers(
201
+ config.servers,
202
+ config.examples || {},
203
+ config.tags || {}
204
+ );
205
+ const connected = cachedServers.filter((s) => s.status === "connected").length;
206
+ const tools = cachedServers.reduce((acc, s) => acc + s.tools.length, 0);
207
+ console.log(chalk.green(` \u2713 ${connected}/${cachedServers.length} servers \xB7 ${tools} tools`));
208
+ broadcastToClients({ type: "reload" });
209
+ } catch (err) {
210
+ console.error(chalk.red(` \u2717 ${err.message}`));
211
+ }
212
+ }
213
+ function broadcastToClients(data) {
214
+ const msg = JSON.stringify(data);
215
+ wss.clients.forEach((c) => {
216
+ if (c.readyState === 1) c.send(msg);
217
+ });
218
+ }
219
+ app.get("/api/servers", async (_req, res) => {
220
+ try {
221
+ res.json({
222
+ title: cachedConfig?.title || "MCP Playbook",
223
+ description: cachedConfig?.description || "",
224
+ theme: cachedConfig?.theme || {},
225
+ servers: cachedServers
226
+ });
227
+ } catch (err) {
228
+ res.status(500).json({ error: err.message });
229
+ }
230
+ });
231
+ app.post("/api/refresh", async (_req, res) => {
232
+ await refreshServers();
233
+ res.json({ ok: true });
234
+ });
235
+ app.post("/api/execute", async (req, res) => {
236
+ const { serverId, toolName, input } = req.body;
237
+ if (!cachedConfig) return res.status(503).json({ error: "Config not loaded" });
238
+ const idx = parseInt(serverId.replace("server-", ""));
239
+ const serverConfig = cachedConfig.servers[idx];
240
+ if (!serverConfig) return res.status(404).json({ error: `Server not found: ${serverId}` });
241
+ const result = await executeTool(serverConfig, toolName, input || {});
242
+ res.json(result);
243
+ });
244
+ app.get("*", (_req, res) => {
245
+ const indexPath = path.join(clientDist, "index.html");
246
+ if (fs.existsSync(indexPath)) {
247
+ res.sendFile(indexPath);
248
+ } else {
249
+ res.send(fallbackHTML(options.port));
250
+ }
251
+ });
252
+ wss.on("connection", (ws) => {
253
+ ws.send(JSON.stringify({ type: "connected" }));
254
+ });
255
+ const watcher = chokidar.watch(options.configPath, { ignoreInitial: true });
256
+ watcher.on("change", () => {
257
+ console.log(chalk.dim("\n Config changed \u2014 refreshing..."));
258
+ refreshServers();
259
+ });
260
+ await new Promise((resolve, reject) => {
261
+ server.on("error", (err) => {
262
+ if (err.code === "EADDRINUSE") {
263
+ reject(new Error(`Port ${options.port} is already in use. Try: --port ${options.port + 1}`));
264
+ } else {
265
+ reject(err);
266
+ }
267
+ });
268
+ server.listen(options.port, () => resolve());
269
+ });
270
+ await refreshServers();
271
+ const url = `http://localhost:${options.port}`;
272
+ console.log(chalk.dim(`
273
+ Local: `) + chalk.cyan(url));
274
+ console.log(chalk.dim(` Config: ${options.configPath}
275
+ `));
276
+ if (options.open && hasClient) {
277
+ try {
278
+ await open(url);
279
+ } catch {
280
+ }
281
+ }
282
+ return server;
283
+ }
284
+ export {
285
+ startDevServer
286
+ };
287
+ //# sourceMappingURL=dev-server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/dev-server.ts","../../src/server/connector.ts"],"sourcesContent":["/**\n * src/server/dev-server.ts\n *\n * The development server.\n * Serves the React UI + REST API + WebSocket hot reload.\n */\n\nimport express from 'express'\nimport { createServer } from 'http'\nimport { WebSocketServer } from 'ws'\nimport path from 'path'\nimport fs from 'fs'\nimport chalk from 'chalk'\nimport chokidar from 'chokidar'\nimport open from 'open'\nimport { discoverAllServers, executeTool } from './connector.js'\nimport type { PlaybookConfig } from '../config.js'\n\ninterface DevServerOptions {\n port: number\n configPath: string\n open: boolean\n}\n\n// ── Resolve client dist path robustly ───────────────────────────────\n// Works both when run from source (via ts-node) and from compiled dist/\nfunction resolveClientDist(): string {\n // __dirname in CJS = directory of the compiled file\n // When compiled: dist/server/dev-server.js → dist/client/\n const fromCompiled = path.join(__dirname, '../client')\n if (fs.existsSync(fromCompiled)) return fromCompiled\n\n // Fallback: look relative to package root\n const fromRoot = path.join(__dirname, '../../dist/client')\n if (fs.existsSync(fromRoot)) return fromRoot\n\n return fromCompiled // return anyway — will be checked later\n}\n\n// ── Minimal fallback HTML when client is not built ───────────────────\nfunction fallbackHTML(port: number): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\"/>\n <title>MCP Playbook</title>\n <style>\n body { font-family: monospace; background: #080808; color: #e2e0d8;\n display: flex; align-items: center; justify-content: center;\n height: 100vh; margin: 0; flex-direction: column; gap: 12px; }\n code { background: #1a1a2e; color: #a39cf0; padding: 4px 10px;\n border-radius: 4px; font-size: 14px; }\n p { color: #5a5856; font-size: 13px; }\n </style>\n</head>\n<body>\n <div style=\"font-size:32px\">⚡</div>\n <h2 style=\"margin:0\">MCP Playbook API is running</h2>\n <p>The React UI bundle was not found at <code>dist/client/</code></p>\n <p>Run <code>npm run build:client</code> inside the mcp-playbook library to build it.</p>\n <p>API available at <code>http://localhost:${port}/api/servers</code></p>\n</body>\n</html>`\n}\n\nexport async function startDevServer(options: DevServerOptions) {\n const app = express()\n const server = createServer(app)\n const wss = new WebSocketServer({ server })\n\n app.use(express.json())\n\n // ── Serve static client bundle ──────────────────────────────────\n const clientDist = resolveClientDist()\n const hasClient = fs.existsSync(path.join(clientDist, 'index.html'))\n\n if (hasClient) {\n app.use(express.static(clientDist))\n console.log(chalk.dim(` Client: serving from ${clientDist}`))\n } else {\n console.log(chalk.yellow(` Warning: client bundle not found at ${clientDist}`))\n console.log(chalk.dim(' API is available but UI will show a placeholder'))\n }\n\n // ── Config loader ───────────────────────────────────────────────\n async function loadConfig(): Promise<PlaybookConfig> {\n // Clear module cache for hot reload\n const key = require.resolve(options.configPath)\n delete require.cache[key]\n\n // Try TypeScript config via ts-node/register if available\n try {\n return require(options.configPath).default\n } catch {\n // ts-node not available — try pre-compiled JS version\n const jsPath = options.configPath.replace(/\\.ts$/, '.js')\n if (fs.existsSync(jsPath)) {\n delete require.cache[require.resolve(jsPath)]\n return require(jsPath).default\n }\n throw new Error(\n `Cannot load config: ${options.configPath}\\n` +\n ` Make sure ts-node is installed: npm install -D ts-node\\n` +\n ` Or compile your config to JS first.`\n )\n }\n }\n\n // ── Discover servers ────────────────────────────────────────────\n let cachedServers: Awaited<ReturnType<typeof discoverAllServers>> = []\n let cachedConfig: PlaybookConfig | null = null\n\n async function refreshServers() {\n try {\n const config = await loadConfig()\n cachedConfig = config\n cachedServers = await discoverAllServers(\n config.servers,\n config.examples || {},\n config.tags || {}\n )\n const connected = cachedServers.filter(s => s.status === 'connected').length\n const tools = cachedServers.reduce((acc, s) => acc + s.tools.length, 0)\n console.log(chalk.green(` ✓ ${connected}/${cachedServers.length} servers · ${tools} tools`))\n broadcastToClients({ type: 'reload' })\n } catch (err: any) {\n console.error(chalk.red(` ✗ ${err.message}`))\n }\n }\n\n function broadcastToClients(data: unknown) {\n const msg = JSON.stringify(data)\n wss.clients.forEach(c => { if (c.readyState === 1) c.send(msg) })\n }\n\n // ── REST API ────────────────────────────────────────────────────\n\n app.get('/api/servers', async (_req, res) => {\n try {\n res.json({\n title: cachedConfig?.title || 'MCP Playbook',\n description: cachedConfig?.description || '',\n theme: cachedConfig?.theme || {},\n servers: cachedServers\n })\n } catch (err: any) {\n res.status(500).json({ error: err.message })\n }\n })\n\n app.post('/api/refresh', async (_req, res) => {\n await refreshServers()\n res.json({ ok: true })\n })\n\n app.post('/api/execute', async (req, res) => {\n const { serverId, toolName, input } = req.body\n if (!cachedConfig) return res.status(503).json({ error: 'Config not loaded' })\n\n const idx = parseInt(serverId.replace('server-', ''))\n const serverConfig = cachedConfig.servers[idx]\n if (!serverConfig) return res.status(404).json({ error: `Server not found: ${serverId}` })\n\n const result = await executeTool(serverConfig, toolName, input || {})\n res.json(result)\n })\n\n // Fallback — serve index.html for SPA routing\n app.get('*', (_req, res) => {\n const indexPath = path.join(clientDist, 'index.html')\n if (fs.existsSync(indexPath)) {\n res.sendFile(indexPath)\n } else {\n res.send(fallbackHTML(options.port))\n }\n })\n\n // ── WebSocket ───────────────────────────────────────────────────\n wss.on('connection', (ws) => {\n ws.send(JSON.stringify({ type: 'connected' }))\n })\n\n // ── Config watcher ──────────────────────────────────────────────\n const watcher = chokidar.watch(options.configPath, { ignoreInitial: true })\n watcher.on('change', () => {\n console.log(chalk.dim('\\n Config changed — refreshing...'))\n refreshServers()\n })\n\n // ── Start ───────────────────────────────────────────────────────\n await new Promise<void>((resolve, reject) => {\n server.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(new Error(`Port ${options.port} is already in use. Try: --port ${options.port + 1}`))\n } else {\n reject(err)\n }\n })\n server.listen(options.port, () => resolve())\n })\n\n await refreshServers()\n\n const url = `http://localhost:${options.port}`\n console.log(chalk.dim(`\\n Local: `) + chalk.cyan(url))\n console.log(chalk.dim(` Config: ${options.configPath}\\n`))\n\n if (options.open && hasClient) {\n try { await open(url) } catch {}\n }\n\n return server\n}\n","/**\n * src/server/connector.ts\n *\n * Connects to MCP servers and discovers their tools.\n * This is the bridge between the user's MCP servers and\n * the Playbook UI.\n *\n * Supports:\n * - stdio transport (spawn a process, talk over stdin/stdout)\n * - HTTP / SSE transport (connect to a running server)\n *\n * Returns a unified DiscoveredServer[] with all tool schemas.\n */\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { ServerConfig, ToolExample } from '../config.js'\n\nexport interface DiscoveredTool {\n name: string\n description: string\n inputSchema: Record<string, unknown>\n examples: ToolExample[]\n tags: string[]\n}\n\nexport interface DiscoveredServer {\n id: string\n name: string\n url: string\n status: 'connected' | 'disconnected' | 'error'\n version: string\n tools: DiscoveredTool[]\n error?: string\n}\n\n/**\n * Connect to all configured servers and discover their tools.\n * Runs all connections in parallel — slow servers don't block fast ones.\n */\nexport async function discoverAllServers(\n servers: ServerConfig[],\n extraExamples: Record<string, ToolExample[]> = {},\n tags: Record<string, string[]> = {}\n): Promise<DiscoveredServer[]> {\n\n // Build a reverse lookup: tool name → tag names\n const toolTagMap: Record<string, string[]> = {}\n for (const [tag, tools] of Object.entries(tags)) {\n for (const tool of tools) {\n if (!toolTagMap[tool]) toolTagMap[tool] = []\n toolTagMap[tool].push(tag)\n }\n }\n\n // Connect to all servers in parallel\n const results = await Promise.allSettled(\n servers.map((server, i) =>\n connectToServer(server, i, extraExamples, toolTagMap)\n )\n )\n\n return results.map((result, i) => {\n if (result.status === 'fulfilled') {\n return result.value\n } else {\n return {\n id: `server-${i}`,\n name: servers[i].name,\n url: servers[i].url || `${servers[i].command} ${(servers[i].args || []).join(' ')}`,\n status: 'error' as const,\n version: 'unknown',\n tools: [],\n error: result.reason?.message || 'Connection failed'\n }\n }\n })\n}\n\nasync function connectToServer(\n config: ServerConfig,\n index: number,\n extraExamples: Record<string, ToolExample[]>,\n toolTagMap: Record<string, string[]>\n): Promise<DiscoveredServer> {\n\n const client = new Client(\n { name: 'mcp-playbook', version: '0.1.0' },\n { capabilities: {} }\n )\n\n // Create the right transport based on config\n let transport\n\n if (config.transport === 'stdio') {\n if (!config.command) throw new Error(`Server \"${config.name}\" needs a command for stdio transport`)\n transport = new StdioClientTransport({\n command: config.command,\n args: config.args || [],\n env: config.env || {},\n cwd: config.cwd || process.cwd()\n })\n } else if (config.transport === 'http') {\n if (!config.url) throw new Error(`Server \"${config.name}\" needs a url for http transport`)\n transport = new StreamableHTTPClientTransport(new URL(config.url), {\n requestInit: { headers: config.headers || {} }\n })\n } else if (config.transport === 'sse') {\n if (!config.url) throw new Error(`Server \"${config.name}\" needs a url for sse transport`)\n transport = new SSEClientTransport(new URL(config.url), {\n requestInit: { headers: config.headers || {} }\n })\n } else {\n throw new Error(`Unknown transport: ${(config as any).transport}`)\n }\n\n await client.connect(transport)\n\n // Fetch tools from the server\n const { tools } = await client.listTools()\n\n // Fetch server info if available\n const serverInfo = client.getServerVersion()\n\n // Map tools to our format\n const discoveredTools: DiscoveredTool[] = tools.map(tool => ({\n name: tool.name,\n description: tool.description || '',\n inputSchema: tool.inputSchema as Record<string, unknown>,\n examples: extraExamples[tool.name] || [],\n tags: toolTagMap[tool.name] || []\n }))\n\n return {\n id: `server-${index}`,\n name: config.name,\n url: config.url || `${config.command} ${(config.args || []).join(' ')}`,\n status: 'connected',\n version: serverInfo?.version || '1.0.0',\n tools: discoveredTools\n }\n}\n\n/**\n * Execute a tool call against a specific server.\n * Used by the Try tab in the UI.\n */\nexport async function executeTool(\n serverConfig: ServerConfig,\n toolName: string,\n input: Record<string, unknown>\n): Promise<{ result: unknown; duration_ms: number; error?: string }> {\n\n const start = Date.now()\n\n try {\n const client = new Client(\n { name: 'mcp-playbook', version: '0.1.0' },\n { capabilities: {} }\n )\n\n let transport\n if (serverConfig.transport === 'stdio') {\n transport = new StdioClientTransport({\n command: serverConfig.command!,\n args: serverConfig.args || [],\n })\n } else {\n transport = new StreamableHTTPClientTransport(new URL(serverConfig.url!))\n }\n\n await client.connect(transport)\n\n const result = await client.callTool({ name: toolName, arguments: input })\n await client.close()\n\n return {\n result,\n duration_ms: Date.now() - start\n }\n } catch (err: any) {\n return {\n result: null,\n duration_ms: Date.now() - start,\n error: err.message\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "mcp-playbook",
3
+ "version": "0.1.0",
4
+ "description": "Playbook for MCP tools — auto-generates interactive docs and a live playground for your MCP servers",
5
+ "keywords": ["mcp", "model-context-protocol", "developer-tools", "documentation", "playground", "playbook", "playbook"],
6
+ "license": "MIT",
7
+
8
+ "bin": {
9
+ "mcp-playbook": "./bin/mcp-playbook.js"
10
+ },
11
+
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.mjs",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "require": "./dist/index.js",
18
+ "import": "./dist/index.mjs",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+
23
+ "files": [
24
+ "dist",
25
+ "bin",
26
+ "README.md"
27
+ ],
28
+
29
+ "scripts": {
30
+ "postinstall": "node ./bin/postinstall.js",
31
+ "dev": "tsup --watch",
32
+ "dev:client": "vite --config vite.client.config.ts",
33
+ "build": "npm run build:server && npm run build:client",
34
+ "build:server": "tsup",
35
+ "build:client": "vite build --config vite.client.config.ts",
36
+ "typecheck": "tsc --noEmit",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.0.0",
42
+ "chalk": "^5.0.0",
43
+ "chokidar": "^3.6.0",
44
+ "commander":"^12.0.0",
45
+ "express": "^4.18.0",
46
+ "open": "^10.0.0",
47
+ "ora": "^8.0.0",
48
+ "ws": "^8.17.0"
49
+ },
50
+
51
+ "devDependencies": {
52
+ "@types/express": "^4.17.21",
53
+ "@types/node": "^20.0.0",
54
+ "@types/react": "^18.0.0",
55
+ "@types/react-dom": "^18.0.0",
56
+ "@types/ws": "^8.5.10",
57
+ "@vitejs/plugin-react": "^4.0.0",
58
+ "react": "^18.0.0",
59
+ "react-dom": "^18.0.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.0.0",
62
+ "vite": "^5.0.0"
63
+ },
64
+
65
+ "engines": {
66
+ "node": ">=18.0.0"
67
+ }
68
+ }