@unfragile/mcp-server 0.1.1 → 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 +114 -4
  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";
@@ -35,7 +37,7 @@ async function searchAPI(query, options = {}) {
35
37
  const controller = new AbortController();
36
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
  });
@@ -99,7 +101,15 @@ function formatMatch(m, rank) {
99
101
  lines.push(` Limitations: ${cap.limitations.join(", ")}`);
100
102
  }
101
103
  }
102
- 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) {
103
113
  lines.push(`\n**Graph Signal:** Matched ${m.matchGraph.timesMatched} times | ${Math.round(m.matchGraph.successRate * 100)}% success`);
104
114
  }
105
115
  lines.push(`\n→ Details: ${m.artifact.pageUrl}`);
@@ -128,7 +138,7 @@ function formatResults(data) {
128
138
  // ─── MCP Server ──────────────────────────────────────────────
129
139
  const server = new McpServer({
130
140
  name: "unfragile",
131
- version: "0.1.0",
141
+ version: "0.2.0",
132
142
  });
133
143
  // Tool 1: General search
134
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.", {
@@ -376,6 +386,106 @@ server.tool("feedback", "Report whether a recommended tool worked or not. This c
376
386
  return { content: [{ type: "text", text: `Error sending feedback: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
377
387
  }
378
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
+ });
379
489
  // ─── Start ───────────────────────────────────────────────────
380
490
  async function main() {
381
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.1",
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",