@unfragile/mcp-server 0.1.0 → 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 (2) hide show
  1. package/dist/index.js +150 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,12 +7,14 @@
7
7
  // the graph learns from every interaction.
8
8
  //
9
9
  // Tools:
10
- // search — Find AI tools by intent/query
10
+ // search — Find AI tools by intent/query (with Match Proof)
11
11
  // find_mcps — Discover MCP servers by capability need
12
12
  // get_artifact — Get full details + capabilities for an artifact
13
13
  // compare — Compare two artifacts side-by-side
14
14
  // find_stack — Assemble a complete harness stack for a use case
15
15
  // feedback — Report success/failure to close the learning loop
16
+ // subscribe — Set up persistent watch for new tools
17
+ // unsubscribe — Cancel a persistent watch
16
18
  // ─────────────────────────────────────────────────────────────
17
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -33,9 +35,9 @@ async function searchAPI(query, options = {}) {
33
35
  if (API_KEY)
34
36
  headers["X-API-Key"] = API_KEY;
35
37
  const controller = new AbortController();
36
- const timeout = setTimeout(() => controller.abort(), 10_000);
38
+ const timeout = setTimeout(() => controller.abort(), 15_000);
37
39
  try {
38
- const res = await fetch(`${API_BASE}/api/v1/search?${params}`, {
40
+ const res = await fetch(`${API_BASE}/api/v1/search?${params}&proof=true`, {
39
41
  headers,
40
42
  signal: controller.signal,
41
43
  });
@@ -54,11 +56,35 @@ async function searchAPI(query, options = {}) {
54
56
  }
55
57
  }
56
58
  // ─── Formatters ──────────────────────────────────────────────
59
+ /** Extract a readable name from a URL when artifact.name is a raw URL */
60
+ function cleanName(name, url) {
61
+ if (!name || /^https?:\/\//.test(name)) {
62
+ // Try to derive name from URL path
63
+ try {
64
+ const u = new URL(url || name);
65
+ const parts = u.pathname.split("/").filter(Boolean);
66
+ // GitHub: owner/repo → repo
67
+ if (u.hostname === "github.com" && parts.length >= 2) {
68
+ return parts[1].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
69
+ }
70
+ // npm/PyPI: package name
71
+ if (parts.length > 0) {
72
+ const last = parts[parts.length - 1];
73
+ return last.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
74
+ }
75
+ }
76
+ catch { }
77
+ // Fallback: strip protocol and trailing slash
78
+ return name.replace(/^https?:\/\//, "").replace(/\/$/, "");
79
+ }
80
+ return name;
81
+ }
57
82
  function formatMatch(m, rank) {
58
83
  const lines = [];
84
+ const name = cleanName(m.artifact.name, m.artifact.url);
59
85
  const verified = m.artifact.verified ? " ✓" : "";
60
86
  const pricing = m.artifact.pricing.free ? "Free" : m.artifact.pricing.model;
61
- lines.push(`### ${rank}. ${m.artifact.name}${verified}`);
87
+ lines.push(`### ${rank}. ${name}${verified}`);
62
88
  lines.push(`**Type:** ${m.artifact.type} | **Score:** ${m.compositeScore}/100 | **Rank:** ${m.artifact.unfragileRank}/100 | **Pricing:** ${pricing}`);
63
89
  lines.push(`**URL:** ${m.artifact.url}`);
64
90
  if (m.artifact.description)
@@ -75,7 +101,15 @@ function formatMatch(m, rank) {
75
101
  lines.push(` Limitations: ${cap.limitations.join(", ")}`);
76
102
  }
77
103
  }
78
- if (m.matchGraph.timesMatched > 0) {
104
+ if (m.matchProof) {
105
+ lines.push(`\n**Match Proof:**`);
106
+ lines.push(` ${m.matchProof.reasoning}`);
107
+ lines.push(` Confidence: ${m.matchProof.confidence.score}/100 (${m.matchProof.confidence.topDimension})`);
108
+ if (m.matchProof.evidence.timesMatched > 0) {
109
+ lines.push(` Evidence: ${m.matchProof.evidence.timesMatched} matches, ${Math.round(m.matchProof.evidence.successRate * 100)}% success`);
110
+ }
111
+ }
112
+ else if (m.matchGraph.timesMatched > 0) {
79
113
  lines.push(`\n**Graph Signal:** Matched ${m.matchGraph.timesMatched} times | ${Math.round(m.matchGraph.successRate * 100)}% success`);
80
114
  }
81
115
  lines.push(`\n→ Details: ${m.artifact.pageUrl}`);
@@ -104,7 +138,7 @@ function formatResults(data) {
104
138
  // ─── MCP Server ──────────────────────────────────────────────
105
139
  const server = new McpServer({
106
140
  name: "unfragile",
107
- version: "0.1.0",
141
+ version: "0.2.0",
108
142
  });
109
143
  // Tool 1: General search
110
144
  server.tool("search", "Search the Unfragile match graph for AI tools, frameworks, APIs, MCP servers, agents, and more. Returns ranked results with capability matches and graph signals. Every query feeds the graph.", {
@@ -150,7 +184,8 @@ server.tool("get_artifact", "Get full details and capabilities for a specific AI
150
184
  return { content: [{ type: "text", text: `No artifact found matching "${name}".` }] };
151
185
  }
152
186
  const lines = [];
153
- lines.push(`# ${match.artifact.name}`);
187
+ const artName = cleanName(match.artifact.name, match.artifact.url);
188
+ lines.push(`# ${artName}`);
154
189
  lines.push(`**Type:** ${match.artifact.type} | **UnfragileRank:** ${match.artifact.unfragileRank}/100`);
155
190
  lines.push(`**URL:** ${match.artifact.url}`);
156
191
  lines.push(`**Verified:** ${match.artifact.verified ? "Yes ✓" : "No"}`);
@@ -200,10 +235,12 @@ server.tool("compare", "Compare two AI artifacts side-by-side. Shows capabilitie
200
235
  if (!matchA && !matchB) {
201
236
  return { content: [{ type: "text", text: `Neither "${artifact_a}" nor "${artifact_b}" found.` }] };
202
237
  }
238
+ const nameA = matchA ? cleanName(matchA.artifact.name, matchA.artifact.url) : artifact_a;
239
+ const nameB = matchB ? cleanName(matchB.artifact.name, matchB.artifact.url) : artifact_b;
203
240
  const lines = [];
204
- lines.push(`# Compare: ${matchA?.artifact.name || artifact_a} vs ${matchB?.artifact.name || artifact_b}\n`);
241
+ lines.push(`# Compare: ${nameA} vs ${nameB}\n`);
205
242
  const row = (label, a, b) => `| ${label} | ${a} | ${b} |`;
206
- lines.push(`| | ${matchA?.artifact.name || "Not found"} | ${matchB?.artifact.name || "Not found"} |`);
243
+ lines.push(`| | ${nameA} | ${nameB} |`);
207
244
  lines.push(`|---|---|---|`);
208
245
  if (matchA && matchB) {
209
246
  lines.push(row("Type", matchA.artifact.type, matchB.artifact.type));
@@ -212,11 +249,11 @@ server.tool("compare", "Compare two AI artifacts side-by-side. Shows capabilitie
212
249
  lines.push(row("Verified", matchA.artifact.verified ? "✓" : "No", matchB.artifact.verified ? "✓" : "No"));
213
250
  lines.push(row("Times Matched", String(matchA.matchGraph.timesMatched), String(matchB.matchGraph.timesMatched)));
214
251
  lines.push(row("Capabilities", String(matchA.capabilities.length), String(matchB.capabilities.length)));
215
- lines.push(`\n## ${matchA.artifact.name} Capabilities`);
252
+ lines.push(`\n## ${nameA} Capabilities`);
216
253
  for (const cap of matchA.capabilities) {
217
254
  lines.push(`- **${cap.name}**: ${cap.description.slice(0, 150)}`);
218
255
  }
219
- lines.push(`\n## ${matchB.artifact.name} Capabilities`);
256
+ lines.push(`\n## ${nameB} Capabilities`);
220
257
  for (const cap of matchB.capabilities) {
221
258
  lines.push(`- **${cap.name}**: ${cap.description.slice(0, 150)}`);
222
259
  }
@@ -275,9 +312,10 @@ server.tool("find_stack", "Assemble a complete AI harness stack for a use case.
275
312
  totalTools += matches.length;
276
313
  lines.push(`## ${layer}\n`);
277
314
  for (const m of matches) {
315
+ const mName = cleanName(m.artifact.name, m.artifact.url);
278
316
  const verified = m.artifact.verified ? " ✓" : "";
279
317
  const pricing = m.artifact.pricing.free ? "Free" : m.artifact.pricing.model;
280
- lines.push(`**${m.artifact.name}${verified}** — ${pricing} | Rank: ${m.artifact.unfragileRank}/100`);
318
+ lines.push(`**${mName}${verified}** — ${pricing} | Rank: ${m.artifact.unfragileRank}/100`);
281
319
  if (m.artifact.description)
282
320
  lines.push(`${m.artifact.description.slice(0, 200)}`);
283
321
  if (m.capabilities.length > 0) {
@@ -348,6 +386,106 @@ server.tool("feedback", "Report whether a recommended tool worked or not. This c
348
386
  return { content: [{ type: "text", text: `Error sending feedback: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
349
387
  }
350
388
  });
389
+ // Tool 7: Subscribe — create a monitor
390
+ server.tool("subscribe", "Set up a persistent watch for new AI tools matching a query. Get notified daily when something new appears in the Unfragile graph. Requires at least one notification channel (email or webhook).", {
391
+ query: z.string().min(2).max(500).describe("What to watch for (e.g., 'new MCP server for Postgres', 'framework for building AI agents')"),
392
+ type: z.enum(["agent", "api", "app", "benchmark", "cli", "dataset", "extension", "finetune", "framework", "mcp", "model", "platform", "product", "prompt", "repo", "skill", "template", "webapp", "workflow"]).optional().describe("Only watch for this artifact type"),
393
+ email: z.string().email().optional().describe("Email address for notifications"),
394
+ webhook: z.string().url().optional().describe("Webhook URL for notifications (receives POST with new matches)"),
395
+ }, async ({ query, type, email, webhook }) => {
396
+ log("subscribe", query);
397
+ if (!email && !webhook) {
398
+ return {
399
+ content: [{ type: "text", text: "Error: At least one notification channel required. Provide 'email' and/or 'webhook'." }],
400
+ isError: true,
401
+ };
402
+ }
403
+ try {
404
+ const headers = { "Content-Type": "application/json" };
405
+ if (API_KEY)
406
+ headers["X-API-Key"] = API_KEY;
407
+ const body = {
408
+ query,
409
+ notify: { email, webhook },
410
+ source: SOURCE,
411
+ };
412
+ if (type)
413
+ body.type = type;
414
+ const controller = new AbortController();
415
+ const timeout = setTimeout(() => controller.abort(), 15_000);
416
+ try {
417
+ const res = await fetch(`${API_BASE}/api/v1/monitor`, {
418
+ method: "POST",
419
+ headers,
420
+ body: JSON.stringify(body),
421
+ signal: controller.signal,
422
+ });
423
+ const data = await res.json();
424
+ if (!res.ok) {
425
+ throw new Error(data.error || `API error ${res.status}`);
426
+ }
427
+ const lines = [];
428
+ lines.push(`# Monitor Created`);
429
+ lines.push(`**ID:** ${data.id}`);
430
+ lines.push(`**Watching for:** ${data.query}`);
431
+ if (data.typeFilter)
432
+ lines.push(`**Type filter:** ${data.typeFilter}`);
433
+ lines.push(`**Frequency:** Daily`);
434
+ if (email)
435
+ lines.push(`**Email alerts:** ${email}`);
436
+ if (webhook)
437
+ lines.push(`**Webhook:** ${webhook}`);
438
+ lines.push(`**Expires:** ${data.expiresAt.slice(0, 10)} (90 days)`);
439
+ lines.push(`\n_Save the monitor ID to unsubscribe later._`);
440
+ return { content: [{ type: "text", text: lines.join("\n") }] };
441
+ }
442
+ finally {
443
+ clearTimeout(timeout);
444
+ }
445
+ }
446
+ catch (err) {
447
+ return {
448
+ content: [{ type: "text", text: `Error creating monitor: ${err instanceof Error ? err.message : String(err)}` }],
449
+ isError: true,
450
+ };
451
+ }
452
+ });
453
+ // Tool 8: Unsubscribe — delete a monitor
454
+ server.tool("unsubscribe", "Cancel a persistent watch (monitor). Use the monitor ID returned from subscribe.", {
455
+ monitorId: z.string().min(1).describe("Monitor ID from subscribe (e.g., 'mon_...')"),
456
+ }, async ({ monitorId }) => {
457
+ log("unsubscribe", monitorId);
458
+ try {
459
+ const headers = {};
460
+ if (API_KEY)
461
+ headers["X-API-Key"] = API_KEY;
462
+ const controller = new AbortController();
463
+ const timeout = setTimeout(() => controller.abort(), 15_000);
464
+ try {
465
+ const res = await fetch(`${API_BASE}/api/v1/monitor?id=${encodeURIComponent(monitorId)}`, {
466
+ method: "DELETE",
467
+ headers,
468
+ signal: controller.signal,
469
+ });
470
+ if (!res.ok) {
471
+ const data = await res.json();
472
+ throw new Error(data.error || `API error ${res.status}`);
473
+ }
474
+ return {
475
+ content: [{ type: "text", text: `Monitor ${monitorId} cancelled. You will no longer receive alerts for this watch.` }],
476
+ };
477
+ }
478
+ finally {
479
+ clearTimeout(timeout);
480
+ }
481
+ }
482
+ catch (err) {
483
+ return {
484
+ content: [{ type: "text", text: `Error cancelling monitor: ${err instanceof Error ? err.message : String(err)}` }],
485
+ isError: true,
486
+ };
487
+ }
488
+ });
351
489
  // ─── Start ───────────────────────────────────────────────────
352
490
  async function main() {
353
491
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unfragile/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Unfragile MCP Server — query the match graph for AI from any agent. Find AI tools, assemble harness stacks, compare artifacts.",
5
5
  "keywords": [
6
6
  "mcp",