@ecommclaw/ops-mcp-server 0.2.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.
Files changed (3) hide show
  1. package/README.md +40 -0
  2. package/package.json +19 -0
  3. package/server.js +438 -0
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @ecommclaw/ops-mcp-server
2
+
3
+ MCP server for external EcommClaw Ops module and pipeline development.
4
+
5
+ It is designed to be installed by developers and called by Claude, Cursor, Codex, or any MCP-compatible AI tool.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @ecommclaw/ops-mcp-server
11
+ ```
12
+
13
+ ## Claude MCP Config
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "ecommclaw-ops": {
19
+ "command": "ecommclaw-ops-mcp",
20
+ "env": {
21
+ "ECOMMCLAW_API_BASE": "https://www.ecommclaw.com",
22
+ "ECOMMCLAW_API_KEY": "YOUR_ECOMMCLAW_UUID_OR_API_KEY"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Tools
30
+
31
+ - `list_modules`
32
+ - `get_module_spec`
33
+ - `list_pipeline_templates`
34
+ - `get_pipeline_template`
35
+ - `scaffold_module`
36
+ - `validate_module`
37
+ - `validate_module_source`
38
+ - `inspect_databag_schema`
39
+
40
+ This first version does not expose production run, publish, deploy, delete, or secret-write tools.
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@ecommclaw/ops-mcp-server",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "MCP server for EcommClaw Ops external module and pipeline development",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "ecommclaw-ops-mcp": "server.js"
9
+ },
10
+ "files": [
11
+ "server.js",
12
+ "README.md",
13
+ "package.json"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "license": "UNLICENSED"
19
+ }
package/server.js ADDED
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const API_BASE = String(process.env.ECOMMCLAW_API_BASE || 'https://www.ecommclaw.com').replace(/\/+$/, '');
9
+ const API_KEY = process.env.ECOMMCLAW_API_KEY || process.env.ECOMMCLAW_DEV_API_KEY || '';
10
+ const LOCAL_ROOT = process.env.ECOMMCLAW_OPS_ROOT ? findOpsRoot(process.env.ECOMMCLAW_OPS_ROOT) : null;
11
+ const LOCAL_PROJECT = process.env.ECOMMCLAW_OPS_PROJECT
12
+ || (LOCAL_ROOT ? path.join(LOCAL_ROOT, 'examples', 'experiments', 'sourcing-1688') : null);
13
+
14
+ const TOOLS = [
15
+ {
16
+ name: 'list_modules',
17
+ description: 'List public EcommClaw ops modules from the platform developer API.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: { include_schemas: { type: 'boolean', default: false } },
21
+ },
22
+ },
23
+ {
24
+ name: 'get_module_spec',
25
+ description: 'Return one public module spec, schema, and capabilities.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: { module_id: { type: 'string' } },
29
+ required: ['module_id'],
30
+ },
31
+ },
32
+ {
33
+ name: 'list_pipeline_templates',
34
+ description: 'List public EcommClaw pipeline templates from the platform developer API.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: { include_stages: { type: 'boolean', default: false } },
38
+ },
39
+ },
40
+ {
41
+ name: 'get_pipeline_template',
42
+ description: 'Return one public pipeline template spec.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: { pipeline_id: { type: 'string' } },
46
+ required: ['pipeline_id'],
47
+ },
48
+ },
49
+ {
50
+ name: 'scaffold_module',
51
+ description: 'Create a local SDK module template for an external developer. This does not publish or deploy it.',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ module_id: { type: 'string' },
56
+ module_type: { type: 'string', enum: ['server', 'browser-agent', 'distributed-agent'] },
57
+ target_dir: { type: 'string', description: 'Directory to create the module in.' },
58
+ overwrite: { type: 'boolean', default: false },
59
+ },
60
+ required: ['module_id'],
61
+ },
62
+ },
63
+ {
64
+ name: 'validate_module',
65
+ description: 'Statically validate a local module file against EcommClaw ops SDK rules. Does not execute user code.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ file_path: { type: 'string' },
70
+ module_id: { type: 'string' },
71
+ },
72
+ required: ['file_path'],
73
+ },
74
+ },
75
+ {
76
+ name: 'validate_module_source',
77
+ description: 'Validate module source text through the platform developer API.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ source: { type: 'string' },
82
+ module_id: { type: 'string' },
83
+ },
84
+ required: ['source'],
85
+ },
86
+ },
87
+ {
88
+ name: 'inspect_databag_schema',
89
+ description: 'Return the EcommClaw databag schema, patch rules, artifact rules, and error codes.',
90
+ inputSchema: { type: 'object', properties: {} },
91
+ },
92
+ {
93
+ name: 'list_pipelines',
94
+ description: 'Alias for list_pipeline_templates.',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: { include_stages: { type: 'boolean', default: false } },
98
+ },
99
+ },
100
+ {
101
+ name: 'get_pipeline_spec',
102
+ description: 'Alias for get_pipeline_template.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: { pipeline_id: { type: 'string' } },
106
+ required: ['pipeline_id'],
107
+ },
108
+ },
109
+ ];
110
+
111
+ const handlers = {
112
+ list_modules: async (args) => {
113
+ if (LOCAL_ROOT) return { modules: listLocalModules(args.include_schemas === true), source: 'local' };
114
+ const data = await developerGet(`/api/developer/v1/modules?include_schemas=${args.include_schemas === true ? '1' : '0'}`);
115
+ return { modules: data.modules || [], source: 'platform' };
116
+ },
117
+ get_module_spec: async (args) => {
118
+ if (LOCAL_ROOT) {
119
+ const modules = listLocalModules(true);
120
+ const mod = modules.find(m => m.id === args.module_id || m.name === args.module_id);
121
+ if (!mod) throw new Error(`module not found: ${args.module_id}`);
122
+ return mod;
123
+ }
124
+ const data = await developerGet(`/api/developer/v1/modules/${encodeURIComponent(args.module_id)}`);
125
+ return data.module;
126
+ },
127
+ list_pipeline_templates: async (args) => {
128
+ if (LOCAL_ROOT) return { pipelines: listLocalPipelines(args.include_stages === true), source: 'local' };
129
+ const data = await developerGet(`/api/developer/v1/pipeline-templates?include_stages=${args.include_stages === true ? '1' : '0'}`);
130
+ return { pipelines: data.pipelines || [], source: 'platform' };
131
+ },
132
+ get_pipeline_template: async (args) => {
133
+ if (LOCAL_ROOT) {
134
+ const pipelines = listLocalPipelines(true);
135
+ const spec = pipelines.find(p => p.id === args.pipeline_id);
136
+ if (!spec) throw new Error(`pipeline not found: ${args.pipeline_id}`);
137
+ return spec;
138
+ }
139
+ const data = await developerGet(`/api/developer/v1/pipeline-templates/${encodeURIComponent(args.pipeline_id)}`);
140
+ return data.pipeline;
141
+ },
142
+ list_pipelines: async (args) => handlers.list_pipeline_templates(args),
143
+ get_pipeline_spec: async (args) => handlers.get_pipeline_template(args),
144
+ scaffold_module: async (args) => scaffoldModule(args),
145
+ validate_module: async (args) => validateModuleFile(args.file_path, args.module_id),
146
+ validate_module_source: async (args) => {
147
+ const data = await developerPost('/api/developer/v1/modules/validate', args);
148
+ return data.validation;
149
+ },
150
+ inspect_databag_schema: async () => databagSchema(),
151
+ };
152
+
153
+ async function developerGet(pathname) {
154
+ return developerFetch(pathname, { method: 'GET' });
155
+ }
156
+
157
+ async function developerPost(pathname, body) {
158
+ return developerFetch(pathname, { method: 'POST', body: JSON.stringify(body || {}) });
159
+ }
160
+
161
+ async function developerFetch(pathname, init) {
162
+ const headers = { 'content-type': 'application/json' };
163
+ if (API_KEY) headers.authorization = `Bearer ${API_KEY}`;
164
+ const resp = await fetch(API_BASE + pathname, { ...init, headers });
165
+ const text = await resp.text();
166
+ let json = null;
167
+ try { json = text ? JSON.parse(text) : null; } catch { /* leave null */ }
168
+ if (!resp.ok || (json && json.ok === false)) {
169
+ const msg = json && (json.error || json.message) || `HTTP ${resp.status}`;
170
+ throw new Error(msg);
171
+ }
172
+ return json;
173
+ }
174
+
175
+ function findOpsRoot(start) {
176
+ let cur = path.resolve(start);
177
+ for (let i = 0; i < 8; i++) {
178
+ if (fs.existsSync(path.join(cur, 'engine', 'pipeline_loader.js'))
179
+ && fs.existsSync(path.join(cur, 'sdk', 'defineModule.js'))) return cur;
180
+ const parent = path.dirname(cur);
181
+ if (parent === cur) break;
182
+ cur = parent;
183
+ }
184
+ throw new Error(`ECOMMCLAW_OPS_ROOT is not an ozon-ops root: ${start}`);
185
+ }
186
+
187
+ function listLocalModules(includeSchemas) {
188
+ const modulesDir = path.join(LOCAL_PROJECT, 'modules');
189
+ if (!fs.existsSync(modulesDir)) throw new Error(`modules directory not found: ${modulesDir}`);
190
+ return fs.readdirSync(modulesDir)
191
+ .filter(f => f.endsWith('.js') && !f.startsWith('_'))
192
+ .sort()
193
+ .map(file => {
194
+ const filePath = path.join(modulesDir, file);
195
+ try {
196
+ delete require.cache[require.resolve(filePath)];
197
+ const mod = require(filePath);
198
+ return sanitizeModule(mod, { includeSchemas });
199
+ } catch (e) {
200
+ return { id: path.basename(file, '.js'), name: path.basename(file, '.js'), load_error: e.message };
201
+ }
202
+ });
203
+ }
204
+
205
+ function sanitizeModule(mod, { includeSchemas = false } = {}) {
206
+ const out = {
207
+ id: mod.id || mod.name,
208
+ name: mod.name || mod.id,
209
+ description: mod.description || '',
210
+ version: mod.version || null,
211
+ type: mod.type || (mod.needsClient ? 'browser-agent' : 'server'),
212
+ runtime: mod.runtime || 'inproc',
213
+ needsClient: mod.needsClient === true,
214
+ capabilities: Array.isArray(mod.capabilities) ? mod.capabilities : [],
215
+ sdk: mod.sdk || null,
216
+ };
217
+ if (includeSchemas) {
218
+ out.inputSchema = mod.inputSchema || mod.inputs || null;
219
+ out.outputSchema = mod.outputSchema || mod.outputs || null;
220
+ }
221
+ return out;
222
+ }
223
+
224
+ function listLocalPipelines(includeStages) {
225
+ const pipelinesDir = path.join(LOCAL_PROJECT, 'pipelines');
226
+ if (!fs.existsSync(pipelinesDir)) throw new Error(`pipelines directory not found: ${pipelinesDir}`);
227
+ const { loadFromYamlFile } = require(path.join(LOCAL_ROOT, 'engine', 'pipeline_loader'));
228
+ return fs.readdirSync(pipelinesDir)
229
+ .filter(f => /\.ya?ml$/i.test(f))
230
+ .sort()
231
+ .map(file => {
232
+ const pipe = loadFromYamlFile(path.join(pipelinesDir, file));
233
+ const out = {
234
+ id: pipe.id,
235
+ description: pipe.description || '',
236
+ start: pipe.start,
237
+ stage_count: pipe.stages ? Object.keys(pipe.stages).length : 0,
238
+ };
239
+ if (includeStages) out.stages = pipe.stages;
240
+ return out;
241
+ });
242
+ }
243
+
244
+ function scaffoldModule(args = {}) {
245
+ const moduleId = normalizeModuleId(args.module_id);
246
+ const type = args.module_type || 'server';
247
+ if (!['server', 'browser-agent', 'distributed-agent'].includes(type)) throw new Error(`invalid module_type: ${type}`);
248
+ const dir = path.resolve(args.target_dir || path.join(process.cwd(), 'ecommclaw-modules'));
249
+ fs.mkdirSync(dir, { recursive: true });
250
+ const filePath = path.join(dir, `${moduleId}.js`);
251
+ if (fs.existsSync(filePath) && !args.overwrite) throw new Error(`file already exists: ${filePath}`);
252
+ fs.writeFileSync(filePath, templateFor(moduleId, type));
253
+ return {
254
+ ok: true,
255
+ module_id: moduleId,
256
+ module_type: type,
257
+ file_path: filePath,
258
+ next_steps: [
259
+ 'Edit business logic inside run() or plan/runTask/reduce().',
260
+ 'Run validate_module on the generated file.',
261
+ 'Submit the module for review only after validation passes.',
262
+ ],
263
+ };
264
+ }
265
+
266
+ function normalizeModuleId(input) {
267
+ const id = String(input || '').trim().replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
268
+ if (!id) throw new Error('module_id required');
269
+ return id;
270
+ }
271
+
272
+ function templateFor(moduleId, type) {
273
+ if (type === 'distributed-agent') return `const { defineModule } = require('@ecommclaw/ops-sdk');\n\nmodule.exports = defineModule({\n id: '${moduleId}',\n name: '${moduleId}',\n version: '0.1.0',\n type: 'distributed-agent',\n inputSchema: {\n type: 'object',\n properties: {\n agent_pool: { type: 'array', items: { type: 'string' } },\n max_items: { type: ['number', 'null'], default: null },\n },\n required: ['agent_pool'],\n },\n outputSchema: { type: 'object', properties: { metrics: { type: 'object' } } },\n capabilities: ['databag:read', 'databag:patch'],\n\n async plan(ctx) {\n const items = await ctx.databag.listItems();\n return items.map(item => ({\n task_id: '${moduleId}:' + item.id,\n item_id: item.id,\n payload: { item_id: item.id },\n idempotency_key: '${moduleId}:' + item.id,\n }));\n },\n\n async runTask(ctx, task) {\n return {\n item_id: task.item_id,\n patch: { 'module_outputs.${moduleId}.status': 'done' },\n };\n },\n\n async reduce(ctx, taskResults) {\n await ctx.databag.patchItems(taskResults.map(r => ({ id: r.item_id, patch: r.patch })));\n return { status: 'success', metrics: { total: taskResults.length, success: taskResults.length, skipped: 0, failed: 0 } };\n },\n});\n`;
274
+ const caps = type === 'browser-agent'
275
+ ? "['databag:read', 'databag:patch', 'browser:navigate']"
276
+ : "['databag:read', 'databag:patch']";
277
+ const browserBody = type === 'browser-agent'
278
+ ? "\n // Use ctx.browser.run(command) for browser work; do not call CDP directly.\n"
279
+ : '';
280
+ return `const { defineModule } = require('@ecommclaw/ops-sdk');\n\nmodule.exports = defineModule({\n id: '${moduleId}',\n name: '${moduleId}',\n version: '0.1.0',\n type: '${type}',\n inputSchema: { type: 'object', properties: {} },\n outputSchema: { type: 'object', properties: { metrics: { type: 'object' } } },\n capabilities: ${caps},\n\n async run(ctx) {${browserBody}\n const items = await ctx.databag.listItems();\n await ctx.databag.patchItems(items.map(item => ({\n id: item.id,\n patch: { 'module_outputs.${moduleId}.status': 'done' },\n })));\n return { status: 'success', metrics: { total: items.length, success: items.length, skipped: 0, failed: 0 } };\n },\n});\n`;
281
+ }
282
+
283
+ function validateModuleFile(filePath, expectedId) {
284
+ const abs = path.resolve(filePath);
285
+ if (!fs.existsSync(abs)) throw new Error(`module file not found: ${abs}`);
286
+ const syntax = spawnSync(process.execPath, ['--check', abs], { encoding: 'utf8' });
287
+ if (syntax.status !== 0) {
288
+ return { ok: false, file_path: abs, errors: [{ code: 'syntax_error', message: syntax.stderr || syntax.stdout }], warnings: [] };
289
+ }
290
+ return validateModuleSource(fs.readFileSync(abs, 'utf8'), { module_id: expectedId, file_path: abs });
291
+ }
292
+
293
+ function validateModuleSource(source, opts = {}) {
294
+ const errors = [];
295
+ const warnings = [];
296
+ const moduleId = opts.module_id || extractStringProp(source, 'id') || extractStringProp(source, 'name');
297
+ const name = extractStringProp(source, 'name');
298
+ const version = extractStringProp(source, 'version');
299
+ const type = extractStringProp(source, 'type');
300
+ const caps = extractArrayProp(source, 'capabilities');
301
+
302
+ if (!/defineModule\s*\(/.test(source)) errors.push({ code: 'missing_define_module', message: 'module must use defineModule()' });
303
+ if (!moduleId) errors.push({ code: 'missing_id', message: 'id is required' });
304
+ if (opts.module_id && moduleId && opts.module_id !== moduleId && opts.module_id !== name) {
305
+ errors.push({ code: 'module_id_mismatch', message: `expected ${opts.module_id}, got ${moduleId}` });
306
+ }
307
+ if (!name) errors.push({ code: 'missing_name', message: 'name is required' });
308
+ if (!version) errors.push({ code: 'missing_version', message: 'version is required' });
309
+ if (!['server', 'browser-agent', 'distributed-agent'].includes(type)) {
310
+ errors.push({ code: 'invalid_type', message: 'type must be server, browser-agent, or distributed-agent' });
311
+ }
312
+ if (!/\binputSchema\s*:/.test(source) && !/\binputs\s*:/.test(source)) errors.push({ code: 'missing_input_schema', message: 'inputSchema is required' });
313
+ if (!/\boutputSchema\s*:/.test(source) && !/\boutputs\s*:/.test(source)) errors.push({ code: 'missing_output_schema', message: 'outputSchema is required' });
314
+ if (!/\bcapabilities\s*:/.test(source)) errors.push({ code: 'missing_capabilities', message: 'capabilities array is required' });
315
+ if (type === 'distributed-agent') {
316
+ for (const fn of ['plan', 'runTask', 'reduce']) {
317
+ if (!new RegExp(`\\b${fn}\\s*\\(`).test(source)) errors.push({ code: 'missing_lifecycle', message: `distributed-agent requires ${fn}()` });
318
+ }
319
+ } else if (!/\brun\s*\(/.test(source)) {
320
+ errors.push({ code: 'missing_run', message: `${type || 'module'} requires run(ctx)` });
321
+ }
322
+
323
+ const risky = [
324
+ ['direct_db_access', /\b(better-sqlite3|getDB\(|new\s+Database\s*\()/, 'error'],
325
+ ['filesystem_write', /\b(fs\.writeFile|fs\.appendFile|fs\.rm|fs\.unlink|fs\.rename|createWriteStream)\b/, 'warning'],
326
+ ['process_spawn', /\b(child_process|spawn\(|exec\(|execFile\()/, 'warning'],
327
+ ['env_secret_access', /\bprocess\.env\b/, 'warning'],
328
+ ['large_context_write_risk', /\bcontext\s*=\s*|return\s+\{[\s\S]{0,200}\bitems\s*:/, 'warning'],
329
+ ];
330
+ for (const [code, re, severity] of risky) {
331
+ if (re.test(source)) (severity === 'error' ? errors : warnings).push({ code, message: `${code} detected` });
332
+ }
333
+ if (moduleId) {
334
+ const bad = [...source.matchAll(/module_outputs\.([a-zA-Z0-9_-]+)\./g)]
335
+ .map(m => m[1])
336
+ .filter(ns => ns !== moduleId);
337
+ if (bad.length) {
338
+ errors.push({ code: 'patches_other_module_namespace', message: [...new Set(bad)].join(', ') });
339
+ }
340
+ }
341
+ if (caps && caps.includes('secret:read')) warnings.push({ code: 'secret_capability_requires_review', message: 'secret:read requires manual review' });
342
+
343
+ return {
344
+ ok: errors.length === 0,
345
+ file_path: opts.file_path || null,
346
+ module: { id: moduleId || null, name: name || null, version: version || null, type: type || null, capabilities: caps || [] },
347
+ errors,
348
+ warnings,
349
+ };
350
+ }
351
+
352
+ function extractStringProp(source, key) {
353
+ const re = new RegExp(`\\b${key}\\s*:\\s*['"\`]([^'"\`]+)['"\`]`);
354
+ const m = source.match(re);
355
+ return m ? m[1] : null;
356
+ }
357
+
358
+ function extractArrayProp(source, key) {
359
+ const re = new RegExp(`\\b${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`);
360
+ const m = source.match(re);
361
+ if (!m) return null;
362
+ return [...m[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map(x => x[1]);
363
+ }
364
+
365
+ function databagSchema() {
366
+ return {
367
+ version: '0.1',
368
+ databag: {
369
+ version: '0.1',
370
+ items: [{ id: 'string', type: 'string', source: {}, group_id: 'string?', status: 'string?', errors: [], artifacts: [], module_outputs: {} }],
371
+ groups: [{ id: 'string', type: 'string', source: {}, module_outputs: {} }],
372
+ artifacts: [{ id: 'string', type: 'image|video|html|json|file', path: 'string', mime_type: 'string?', size_bytes: 'number?', sha256: 'string?' }],
373
+ meta: {},
374
+ },
375
+ allowed_item_patch_paths: ['module_outputs.<current_module_id>.*', 'status', 'errors', 'artifacts'],
376
+ forbidden: [
377
+ 'Do not patch another module namespace.',
378
+ 'Do not write large images, videos, HTML, or long logs into main context.',
379
+ 'Do not read or write the database directly from a module.',
380
+ 'Do not manage multi-agent concurrency manually; use distributed-agent lifecycle and agent_pool.',
381
+ ],
382
+ error_codes: [
383
+ 'missing_input', 'invalid_config', 'agent_unavailable', 'captcha_required',
384
+ 'network_error', 'timeout', 'parse_failed', 'no_match', 'validation_failed',
385
+ 'api_rate_limited', 'api_rejected', 'permission_denied', 'quota_exceeded',
386
+ ],
387
+ };
388
+ }
389
+
390
+ function send(obj) {
391
+ process.stdout.write(JSON.stringify(obj) + '\n');
392
+ }
393
+
394
+ function contentResult(value) {
395
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
396
+ }
397
+
398
+ async function handleRequest(req) {
399
+ if (req.method === 'initialize') {
400
+ return {
401
+ protocolVersion: req.params && req.params.protocolVersion || '2024-11-05',
402
+ capabilities: { tools: {} },
403
+ serverInfo: { name: 'ecommclaw-ops-mcp', version: '0.2.0' },
404
+ };
405
+ }
406
+ if (req.method === 'tools/list') return { tools: TOOLS };
407
+ if (req.method === 'tools/call') {
408
+ const name = req.params && req.params.name;
409
+ const args = req.params && req.params.arguments || {};
410
+ if (!handlers[name]) throw new Error(`unknown tool: ${name}`);
411
+ return contentResult(await handlers[name](args));
412
+ }
413
+ if (req.method === 'ping') return {};
414
+ throw new Error(`unsupported method: ${req.method}`);
415
+ }
416
+
417
+ let buffer = '';
418
+ process.stdin.setEncoding('utf8');
419
+ process.stdin.on('data', chunk => {
420
+ buffer += chunk;
421
+ let idx;
422
+ while ((idx = buffer.indexOf('\n')) >= 0) {
423
+ const line = buffer.slice(0, idx).trim();
424
+ buffer = buffer.slice(idx + 1);
425
+ if (!line) continue;
426
+ let req;
427
+ Promise.resolve()
428
+ .then(() => {
429
+ req = JSON.parse(line);
430
+ if (req.id === undefined || req.id === null) return null;
431
+ return handleRequest(req).then(result => send({ jsonrpc: '2.0', id: req.id, result }));
432
+ })
433
+ .catch(e => {
434
+ const id = req && req.id !== undefined ? req.id : null;
435
+ send({ jsonrpc: '2.0', id, error: { code: -32000, message: e.message } });
436
+ });
437
+ }
438
+ });