@deplens/mcp 0.1.7 → 0.1.8

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/src/server.mjs CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
5
8
  let corePromise = null;
9
+ let diffPromise = null;
6
10
 
7
11
  async function loadCore() {
8
12
  if (!corePromise) {
@@ -11,8 +15,13 @@ async function loadCore() {
11
15
  const fallbackUrl = new URL("./core/inspect.mjs", import.meta.url);
12
16
  return await import(fallbackUrl.href);
13
17
  } catch (fallbackError) {
14
- const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
15
- const err = new Error(`Failed to load @deplens/core. Fallback also failed: ${message}`);
18
+ const message =
19
+ fallbackError instanceof Error
20
+ ? fallbackError.message
21
+ : String(fallbackError);
22
+ const err = new Error(
23
+ `Failed to load @deplens/core. Fallback also failed: ${message}`,
24
+ );
16
25
  err.cause = error;
17
26
  throw err;
18
27
  }
@@ -21,22 +30,116 @@ async function loadCore() {
21
30
  return corePromise;
22
31
  }
23
32
 
33
+ async function loadDiff() {
34
+ if (!diffPromise) {
35
+ diffPromise = (async () => {
36
+ // Try @deplens/core first
37
+ try {
38
+ const core = await import("@deplens/core");
39
+ if (core.runDiff) return core.runDiff;
40
+ if (core.default?.runDiff) return core.default.runDiff;
41
+ } catch {
42
+ // @deplens/core not available, try fallback
43
+ }
44
+
45
+ // Fallback to local module
46
+ try {
47
+ const fallbackUrl = new URL("./core/diff.mjs", import.meta.url);
48
+ const mod = await import(fallbackUrl.href);
49
+ return mod.runDiff || mod.default?.runDiff;
50
+ } catch (e) {
51
+ console.error("Failed to load diff module:", e);
52
+ return null;
53
+ }
54
+ })();
55
+ }
56
+ return diffPromise;
57
+ }
58
+
24
59
  const inspectToolSchema = {
25
60
  type: "object",
26
61
  properties: {
27
- target: { type: "string", description: "Package name or import path (e.g. react, next/server)" },
28
- subpath: { type: "string", description: "Optional subpath (e.g. server for next/server)" },
62
+ target: {
63
+ type: "string",
64
+ description: "Package name or import path (e.g. react, next/server)",
65
+ },
66
+ subpath: {
67
+ type: "string",
68
+ description: "Optional subpath (e.g. server for next/server)",
69
+ },
29
70
  filter: { type: "string", description: "Substring filter for exports" },
30
71
  kind: {
31
72
  type: "array",
32
- items: { type: "string", enum: ["function", "class", "object", "constant", "interface", "type"] },
73
+ items: {
74
+ type: "string",
75
+ enum: ["function", "class", "object", "constant", "interface", "type"],
76
+ },
33
77
  description: "Filter by export kind",
34
78
  },
35
- showTypes: { type: "boolean", description: "Show type signatures from .d.ts" },
79
+ showTypes: {
80
+ type: "boolean",
81
+ description: "Show type signatures from .d.ts",
82
+ },
83
+ includeDocs: {
84
+ type: "boolean",
85
+ description: "Include README preview (docs)",
86
+ },
87
+ includeExamples: {
88
+ type: "boolean",
89
+ description: "Include README/examples/@example snippets",
90
+ },
91
+ remote: {
92
+ type: "boolean",
93
+ description: "Download package to cache and inspect that version",
94
+ },
95
+ remoteVersion: {
96
+ type: "string",
97
+ description: "Version for remote download (default: latest)",
98
+ },
99
+ format: {
100
+ type: "string",
101
+ enum: ["text", "json", "object"],
102
+ description: "Output format (default: text)",
103
+ },
104
+ listSections: {
105
+ type: "boolean",
106
+ description: "List available README sections",
107
+ },
108
+ docsSections: {
109
+ type: "array",
110
+ items: { type: "string" },
111
+ description: "Extract specific README sections by name",
112
+ },
113
+ search: {
114
+ type: "string",
115
+ description: "Semantic search query (token matching + JSDoc)",
116
+ },
117
+ maxExports: {
118
+ type: "number",
119
+ description: "Max exports to show (default: 100)",
120
+ },
121
+ maxProps: {
122
+ type: "number",
123
+ description: "Max props per object (default: 10)",
124
+ },
125
+ maxExamples: {
126
+ type: "number",
127
+ description: "Max examples to show (default: 10)",
128
+ },
36
129
  depth: { type: "number", description: "Depth for object inspection (0-5)" },
37
- resolveFrom: { type: "string", description: "Base directory for module resolution" },
38
- rootDir: { type: "string", description: "Working directory for the inspection (default: cwd)" },
39
- jsdoc: { type: "string", enum: ["off", "compact", "full"], description: "JSDoc mode" },
130
+ resolveFrom: {
131
+ type: "string",
132
+ description: "Base directory for module resolution",
133
+ },
134
+ rootDir: {
135
+ type: "string",
136
+ description: "Working directory for the inspection (default: cwd)",
137
+ },
138
+ jsdoc: {
139
+ type: "string",
140
+ enum: ["off", "compact", "full"],
141
+ description: "JSDoc mode",
142
+ },
40
143
  jsdocOutput: {
41
144
  type: "string",
42
145
  enum: ["off", "section", "inline", "only"],
@@ -46,11 +149,17 @@ const inspectToolSchema = {
46
149
  type: "object",
47
150
  properties: {
48
151
  symbols: {
49
- oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
152
+ oneOf: [
153
+ { type: "string" },
154
+ { type: "array", items: { type: "string" } },
155
+ ],
50
156
  },
51
157
  sections: {
52
158
  type: "array",
53
- items: { type: "string", enum: ["summary", "params", "returns", "tags"] },
159
+ items: {
160
+ type: "string",
161
+ enum: ["summary", "params", "returns", "tags"],
162
+ },
54
163
  },
55
164
  tags: {
56
165
  type: "object",
@@ -64,21 +173,256 @@ const inspectToolSchema = {
64
173
  truncate: { type: "string", enum: ["none", "sentence", "word"] },
65
174
  },
66
175
  },
176
+ analyzeSource: {
177
+ type: "boolean",
178
+ description:
179
+ "Analyze source code (.ts/.js) for implementation details, complexity, patterns",
180
+ },
181
+ sourceMaxFiles: {
182
+ type: "number",
183
+ description: "Max source files to analyze (default: 5)",
184
+ },
185
+ sourceIncludeBody: {
186
+ type: "boolean",
187
+ description: "Include function body snippets in output",
188
+ },
67
189
  },
68
190
  required: ["target"],
69
191
  };
70
192
 
193
+ const diffToolSchema = {
194
+ type: "object",
195
+ properties: {
196
+ package: {
197
+ type: "string",
198
+ description: "Package name to compare (e.g. react, zod)",
199
+ },
200
+ from: {
201
+ type: "string",
202
+ description:
203
+ "Source version (e.g. '3.22.0', 'installed'). Default: 'installed'",
204
+ },
205
+ to: {
206
+ type: "string",
207
+ description:
208
+ "Target version (e.g. '3.24.0', 'latest'). Default: 'latest'",
209
+ },
210
+ includeSource: {
211
+ type: "boolean",
212
+ description: "Include source code complexity analysis",
213
+ },
214
+ includeChangelog: {
215
+ type: "boolean",
216
+ description: "Parse and include CHANGELOG.md entries (default: true)",
217
+ },
218
+ filter: {
219
+ type: "string",
220
+ description: "Filter exports by name",
221
+ },
222
+ format: {
223
+ type: "string",
224
+ enum: ["text", "json", "object"],
225
+ description: "Output format: 'text' (default) or 'json'",
226
+ },
227
+
228
+ verbose: {
229
+ type: "boolean",
230
+ description: "Show detailed changes",
231
+ },
232
+ },
233
+ required: ["package"],
234
+ };
235
+
236
+ const inspectToolOutputSchema = {
237
+ type: "object",
238
+ additionalProperties: false,
239
+ properties: {
240
+ schemaVersion: { type: "number" },
241
+ package: { type: ["string", "null"] },
242
+ version: { type: ["string", "null"] },
243
+ description: { type: ["string", "null"] },
244
+ resolution: {
245
+ type: ["object", "null"],
246
+ additionalProperties: false,
247
+ properties: {
248
+ target: { type: ["string", "null"] },
249
+ resolveFrom: { type: ["string", "null"] },
250
+ resolveCwd: { type: ["string", "null"] },
251
+ resolved: { type: ["string", "null"] },
252
+ entrypointPath: { type: ["string", "null"] },
253
+ entrypointExists: { type: "boolean" },
254
+ },
255
+ required: [
256
+ "target",
257
+ "resolveFrom",
258
+ "resolveCwd",
259
+ "resolved",
260
+ "entrypointPath",
261
+ "entrypointExists",
262
+ ],
263
+ },
264
+ exports: {
265
+ type: ["object", "null"],
266
+ additionalProperties: false,
267
+ properties: {
268
+ total: { type: "number" },
269
+ functions: { type: "array", items: { type: "string" } },
270
+ classes: { type: "array", items: { type: "string" } },
271
+ objects: { type: "array", items: { type: "string" } },
272
+ constants: { type: "array", items: { type: "string" } },
273
+ },
274
+ required: ["total", "functions", "classes", "objects", "constants"],
275
+ },
276
+ types: {
277
+ type: ["object", "null"],
278
+ additionalProperties: true,
279
+ },
280
+ docs: {
281
+ type: ["object", "null"],
282
+ additionalProperties: true,
283
+ },
284
+ sections: {
285
+ type: ["array", "null"],
286
+ items: { type: "object" },
287
+ },
288
+ examples: {
289
+ type: ["object", "null"],
290
+ additionalProperties: true,
291
+ },
292
+ meta: {
293
+ type: ["object", "null"],
294
+ additionalProperties: true,
295
+ },
296
+ warnings: { type: "array", items: { type: "string" } },
297
+ },
298
+ required: [
299
+ "schemaVersion",
300
+ "package",
301
+ "version",
302
+ "description",
303
+ "exports",
304
+ "types",
305
+ "docs",
306
+ "sections",
307
+ "examples",
308
+ "resolution",
309
+ "meta",
310
+ "warnings",
311
+ ],
312
+ };
313
+
314
+ const diffToolOutputSchema = {
315
+ type: "object",
316
+ additionalProperties: false,
317
+ properties: {
318
+ schemaVersion: { type: "number" },
319
+ package: { type: "string" },
320
+ from: { type: "string" },
321
+ to: { type: "string" },
322
+ output: { type: ["string", "null"] },
323
+ summary: { type: ["object", "null"], additionalProperties: true },
324
+ changes: { type: ["array", "null"], items: { type: "object" } },
325
+ },
326
+ required: [
327
+ "schemaVersion",
328
+ "package",
329
+ "from",
330
+ "to",
331
+ "output",
332
+ "summary",
333
+ "changes",
334
+ ],
335
+ };
336
+
337
+ function formatInspectSummary(result) {
338
+ if (!result) return "No results";
339
+
340
+ const lines = [];
341
+
342
+ if (result.package) {
343
+ lines.push(
344
+ `📦 ${result.package}${result.version ? ` v${result.version}` : ""}`,
345
+ );
346
+ }
347
+
348
+ if (result.description) {
349
+ lines.push(` ${result.description}`);
350
+ }
351
+
352
+ if (result.exports) {
353
+ const parts = [];
354
+ if (result.exports.functions?.length)
355
+ parts.push(`${result.exports.functions.length} functions`);
356
+ if (result.exports.classes?.length)
357
+ parts.push(`${result.exports.classes.length} classes`);
358
+ if (result.exports.objects?.length)
359
+ parts.push(`${result.exports.objects.length} objects`);
360
+ if (result.exports.constants?.length)
361
+ parts.push(`${result.exports.constants.length} constants`);
362
+ if (parts.length) {
363
+ lines.push(` 🔑 ${result.exports.total} exports: ${parts.join(", ")}`);
364
+ }
365
+ }
366
+
367
+ if (result.types) {
368
+ const hasTypes = Object.values(result.types).some(
369
+ (v) => v && Object.keys(v).length > 0,
370
+ );
371
+ if (hasTypes) {
372
+ lines.push(` 🔬 Type definitions available`);
373
+ }
374
+ }
375
+
376
+ if (result.warnings?.length) {
377
+ lines.push(` ⚠️ ${result.warnings.length} warning(s)`);
378
+ }
379
+
380
+ return lines.join("\n") || "Inspection complete";
381
+ }
382
+
383
+ function formatDiffSummary(summary, packageName) {
384
+ const lines = [];
385
+
386
+ if (packageName) {
387
+ lines.push(`📦 ${packageName}`);
388
+ }
389
+
390
+ if (summary) {
391
+ const parts = [];
392
+ if (summary.breaking) parts.push(`${summary.breaking} breaking`);
393
+ if (summary.warnings) parts.push(`${summary.warnings} warnings`);
394
+ if (summary.additions) parts.push(`${summary.additions} additions`);
395
+ if (summary.removals) parts.push(`${summary.removals} removals`);
396
+ if (parts.length) {
397
+ lines.push(` 📊 ${parts.join(", ")}`);
398
+ } else {
399
+ lines.push(` 📊 No changes detected`);
400
+ }
401
+ }
402
+
403
+ return lines.join("\n") || "Diff complete";
404
+ }
405
+
71
406
  const tools = [
72
407
  {
73
408
  name: "deplens.inspect",
74
- description: "Inspect exports and types for an installed npm package.",
409
+ description:
410
+ "Inspect a package to get types, exports, docs, examples, and resolution info.",
75
411
  inputSchema: inspectToolSchema,
412
+ outputSchema: inspectToolOutputSchema,
413
+ },
414
+ {
415
+ name: "deplens.diff",
416
+ description:
417
+ "Compare two versions of an npm package. Detects breaking changes, additions, and modifications. Parses CHANGELOG.md when available.",
418
+ inputSchema: diffToolSchema,
419
+ outputSchema: diffToolOutputSchema,
76
420
  },
77
421
  ];
78
422
 
79
423
  const server = new Server(
80
424
  { name: "deplens", version: "0.1.6" },
81
- { capabilities: { tools: {} } }
425
+ { capabilities: { tools: {} } },
82
426
  );
83
427
 
84
428
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -88,36 +432,236 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
88
432
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
89
433
  const { name, arguments: args } = request.params;
90
434
  try {
91
- if (name !== "deplens.inspect" && name !== "deplens_inspect") {
92
- throw new Error(`Unknown tool: ${name}`);
435
+ // Handle inspect tool
436
+ if (name === "deplens.inspect" || name === "deplens_inspect") {
437
+ const DEBUG = process.env.DEPLENS_DEBUG === "true";
438
+ if (DEBUG) {
439
+ console.error("[DEPLENS DEBUG] inspect args:", JSON.stringify(args, null, 2));
440
+ }
441
+ const rootDir =
442
+ args?.rootDir || process.env.DEPLENS_ROOT || process.cwd();
443
+ const target = args?.subpath
444
+ ? `${args.target}/${args.subpath}`
445
+ : args?.target;
446
+ if (!target) throw new Error("Missing required field: target");
447
+
448
+ const core = await loadCore();
449
+ const runInspect = core.runInspect || core.default?.runInspect;
450
+ if (!runInspect) {
451
+ throw new Error("Failed to load runInspect from @deplens/core");
452
+ }
453
+ // Always get structured output first
454
+ const structuredOutput = await runInspect({
455
+ target,
456
+ filter: args?.filter,
457
+ showTypes: args?.showTypes,
458
+ includeDocs: args?.includeDocs,
459
+ includeExamples: args?.includeExamples,
460
+ remote: args?.remote,
461
+ remoteVersion: args?.remoteVersion,
462
+ jsdoc: args?.jsdoc,
463
+ jsdocOutput: args?.jsdocOutput,
464
+ jsdocQuery: args?.jsdocQuery,
465
+ kind: args?.kind,
466
+ depth: args?.depth,
467
+ resolveFrom: args?.resolveFrom,
468
+ cwd: rootDir,
469
+ analyzeSource: args?.analyzeSource,
470
+ sourceMaxFiles: args?.sourceMaxFiles,
471
+ sourceIncludeBody: args?.sourceIncludeBody,
472
+ // It's always safe to return object in MCP
473
+ format: "object",
474
+ listSections: args?.listSections,
475
+ docsSections: args?.docsSections,
476
+ search: args?.search,
477
+ maxExports: args?.maxExports,
478
+ maxProps: args?.maxProps,
479
+ maxExamples: args?.maxExamples,
480
+ });
481
+
482
+ // Generate text output based on requested format
483
+ let text;
484
+ if (args?.format === "json") {
485
+ // Return JSON string
486
+ text = JSON.stringify(structuredOutput, null, 2);
487
+ } else if (args?.format === "text" || !args?.format) {
488
+ // Return pretty formatted text
489
+ text = await runInspect({
490
+ target,
491
+ filter: args?.filter,
492
+ showTypes: args?.showTypes,
493
+ includeDocs: args?.includeDocs,
494
+ includeExamples: args?.includeExamples,
495
+ remote: args?.remote,
496
+ remoteVersion: args?.remoteVersion,
497
+ jsdoc: args?.jsdoc,
498
+ jsdocOutput: args?.jsdocOutput,
499
+ jsdocQuery: args?.jsdocQuery,
500
+ kind: args?.kind,
501
+ depth: args?.depth,
502
+ resolveFrom: args?.resolveFrom,
503
+ cwd: rootDir,
504
+ analyzeSource: args?.analyzeSource,
505
+ sourceMaxFiles: args?.sourceMaxFiles,
506
+ sourceIncludeBody: args?.sourceIncludeBody,
507
+ format: "text",
508
+ listSections: args?.listSections,
509
+ docsSections: args?.docsSections,
510
+ search: args?.search,
511
+ maxExports: args?.maxExports,
512
+ maxProps: args?.maxProps,
513
+ maxExamples: args?.maxExamples,
514
+ });
515
+ } else {
516
+ // Default: summary
517
+ text = formatInspectSummary(structuredOutput);
518
+ }
519
+
520
+ // MCP best practice: ensure text is always a valid string
521
+ const validText = typeof text === "string" ? text : String(text || "");
522
+
523
+ // MCP best practice: ensure structuredContent is always an object, never a string
524
+ const validStructured =
525
+ typeof structuredOutput === "object" && structuredOutput !== null
526
+ ? structuredOutput
527
+ : {
528
+ schemaVersion: 1,
529
+ package: null,
530
+ version: null,
531
+ description: null,
532
+ exports: null,
533
+ types: null,
534
+ docs: null,
535
+ sections: null,
536
+ examples: null,
537
+ resolution: null,
538
+ meta: { target: target || null },
539
+ warnings: [
540
+ "Invalid output format received from runInspect",
541
+ ],
542
+ };
543
+
544
+ // Final validation before returning (critical for MCP spec compliance)
545
+ if (typeof validStructured !== "object" || validStructured === null) {
546
+ throw new Error(
547
+ `CRITICAL: structuredContent is not an object (type: ${typeof validStructured})`
548
+ );
549
+ }
550
+
551
+ if (DEBUG) {
552
+ console.error("[DEPLENS DEBUG] Response:", {
553
+ textType: typeof validText,
554
+ textLength: validText.length,
555
+ structuredType: typeof validStructured,
556
+ structuredKeys: Object.keys(validStructured),
557
+ });
558
+ }
559
+
560
+ return {
561
+ content: [{ type: "text", text: validText }],
562
+ structuredContent: validStructured,
563
+ isError: false,
564
+ };
93
565
  }
94
566
 
95
- const rootDir = args?.rootDir || process.env.DEPLENS_ROOT || process.cwd();
96
- const target = args?.subpath ? `${args.target}/${args.subpath}` : args?.target;
97
- if (!target) throw new Error("Missing required field: target");
567
+ // Handle diff tool
568
+ if (name === "deplens.diff" || name === "deplens_diff") {
569
+ const packageName = args?.package;
570
+ if (!packageName) throw new Error("Missing required field: package");
571
+
572
+ const runDiff = await loadDiff();
573
+ if (!runDiff) {
574
+ throw new Error(
575
+ "Diff functionality not available. Missing diff.mjs module.",
576
+ );
577
+ }
98
578
 
99
- const core = await loadCore();
100
- const runInspect = core.runInspect || core.default?.runInspect;
101
- if (!runInspect) {
102
- throw new Error("Failed to load runInspect from @deplens/core");
579
+ const rootDir =
580
+ args?.rootDir || process.env.DEPLENS_ROOT || process.cwd();
581
+ const result = await runDiff({
582
+ package: packageName,
583
+ from: args?.from || "installed",
584
+ to: args?.to || "latest",
585
+ projectDir: rootDir,
586
+ includeSource: args?.includeSource || false,
587
+ includeChangelog: args?.includeChangelog !== false,
588
+ filter: args?.filter,
589
+ format: args?.format || "text",
590
+ verbose: args?.verbose || false,
591
+ colors: false, // No ANSI colors in MCP output
592
+ });
593
+
594
+ const diffSummary = result?.diff?.summary ?? result?.summary ?? null;
595
+ const diffChanges = result?.changes
596
+ ? result.changes
597
+ : result?.diff
598
+ ? [
599
+ ...(result.diff.breaking || []),
600
+ ...(result.diff.warnings || []),
601
+ ...(result.diff.additions || []),
602
+ ...(result.diff.info || []),
603
+ ]
604
+ : null;
605
+
606
+ const textOutput =
607
+ args?.format === "text" || !args?.format
608
+ ? result?.output || ""
609
+ : formatDiffSummary(diffSummary, packageName);
610
+
611
+ const structured =
612
+ typeof result === "object" && result
613
+ ? {
614
+ schemaVersion: 1,
615
+ package: result.package || packageName,
616
+ from: result.from || args?.from || "installed",
617
+ to: result.to || args?.to || "latest",
618
+ output: result.output || null,
619
+ summary: diffSummary,
620
+ changes: diffChanges,
621
+ }
622
+ : {
623
+ schemaVersion: 1,
624
+ package: packageName,
625
+ from: args?.from || "installed",
626
+ to: args?.to || "latest",
627
+ output: result?.output || String(result),
628
+ summary: null,
629
+ changes: null,
630
+ };
631
+
632
+ return {
633
+ content: [{ type: "text", text: textOutput }],
634
+ structuredContent: structured,
635
+ isError: false,
636
+ };
103
637
  }
104
- const output = await runInspect({
105
- target,
106
- filter: args?.filter,
107
- showTypes: args?.showTypes,
108
- jsdoc: args?.jsdoc,
109
- jsdocOutput: args?.jsdocOutput,
110
- jsdocQuery: args?.jsdocQuery,
111
- kind: args?.kind,
112
- depth: args?.depth,
113
- resolveFrom: args?.resolveFrom,
114
- cwd: rootDir,
115
- });
116
638
 
117
- return { content: [{ type: "text", text: output }] };
639
+ throw new Error(`Unknown tool: ${name}`);
118
640
  } catch (error) {
119
641
  const message = error instanceof Error ? error.message : String(error);
120
- return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
642
+
643
+ // MCP best practice: always return structuredContent as an object
644
+ const errorStructured = {
645
+ schemaVersion: 1,
646
+ error: message,
647
+ package: null,
648
+ version: null,
649
+ description: null,
650
+ exports: null,
651
+ types: null,
652
+ docs: null,
653
+ sections: null,
654
+ examples: null,
655
+ resolution: null,
656
+ meta: null,
657
+ warnings: [message],
658
+ };
659
+
660
+ return {
661
+ content: [{ type: "text", text: `Error: ${message}` }],
662
+ structuredContent: errorStructured,
663
+ isError: true,
664
+ };
121
665
  }
122
666
  });
123
667