@unfragile/mcp-server 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.
- package/README.md +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +360 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @unfragile/mcp-server
|
|
2
|
+
|
|
3
|
+
Query the [Unfragile match graph](https://unfragile.ai) from any AI agent. Find AI tools, assemble harness stacks, compare artifacts — every query feeds the graph.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
### Claude Code
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add unfragile -- npx -y @unfragile/mcp-server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Claude Desktop
|
|
14
|
+
|
|
15
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"unfragile": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@unfragile/mcp-server"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Cursor
|
|
29
|
+
|
|
30
|
+
Add to `.cursor/mcp.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"unfragile": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "@unfragile/mcp-server"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Windsurf
|
|
44
|
+
|
|
45
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"unfragile": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@unfragile/mcp-server"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
| Tool | Description |
|
|
61
|
+
|------|-------------|
|
|
62
|
+
| `search` | Find AI tools by intent. "best framework for building AI agents" |
|
|
63
|
+
| `find_mcps` | Discover MCP servers by capability. "Postgres + Slack integration" |
|
|
64
|
+
| `get_artifact` | Get full details + capabilities for a specific artifact |
|
|
65
|
+
| `compare` | Compare two artifacts side-by-side |
|
|
66
|
+
| `find_stack` | Assemble a complete harness stack for a use case |
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
Once installed, ask your agent:
|
|
71
|
+
|
|
72
|
+
- "Find me MCP servers for Postgres and Slack"
|
|
73
|
+
- "What's the best framework for building AI agents?"
|
|
74
|
+
- "Compare LangChain vs CrewAI"
|
|
75
|
+
- "I'm building a customer support agent — what tools do I need?"
|
|
76
|
+
- "Get details on Cursor's capabilities"
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
The MCP server calls the [Unfragile API](https://unfragile.ai/api/v1/search) under the hood. Every query becomes a match record in the graph — the graph learns from every interaction, regardless of where it happens.
|
|
81
|
+
|
|
82
|
+
10,000+ AI artifacts. 33,000+ capabilities. The match graph for AI.
|
|
83
|
+
|
|
84
|
+
## Environment Variables
|
|
85
|
+
|
|
86
|
+
| Variable | Description | Default |
|
|
87
|
+
|----------|-------------|---------|
|
|
88
|
+
| `UNFRAGILE_API_URL` | API base URL | `https://unfragile.ai` |
|
|
89
|
+
| `UNFRAGILE_API_KEY` | API key for higher rate limits | (none) |
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// Unfragile MCP Server
|
|
4
|
+
//
|
|
5
|
+
// Query the match graph for AI from any agent (Claude Code,
|
|
6
|
+
// Cursor, Windsurf, etc). Every query feeds the graph —
|
|
7
|
+
// the graph learns from every interaction.
|
|
8
|
+
//
|
|
9
|
+
// Tools:
|
|
10
|
+
// search — Find AI tools by intent/query
|
|
11
|
+
// find_mcps — Discover MCP servers by capability need
|
|
12
|
+
// get_artifact — Get full details + capabilities for an artifact
|
|
13
|
+
// compare — Compare two artifacts side-by-side
|
|
14
|
+
// find_stack — Assemble a complete harness stack for a use case
|
|
15
|
+
// feedback — Report success/failure to close the learning loop
|
|
16
|
+
// ─────────────────────────────────────────────────────────────
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
const API_BASE = process.env.UNFRAGILE_API_URL || "https://unfragile.ai";
|
|
21
|
+
const API_KEY = process.env.UNFRAGILE_API_KEY || "";
|
|
22
|
+
const SOURCE = "mcp-server";
|
|
23
|
+
function log(tool, query) {
|
|
24
|
+
console.error(`[unfragile] ${tool}: ${query}`);
|
|
25
|
+
}
|
|
26
|
+
async function searchAPI(query, options = {}) {
|
|
27
|
+
const params = new URLSearchParams({ q: query, source: SOURCE });
|
|
28
|
+
if (options.limit)
|
|
29
|
+
params.set("limit", String(options.limit));
|
|
30
|
+
if (options.type)
|
|
31
|
+
params.set("type", options.type);
|
|
32
|
+
const headers = { Accept: "application/json" };
|
|
33
|
+
if (API_KEY)
|
|
34
|
+
headers["X-API-Key"] = API_KEY;
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`${API_BASE}/api/v1/search?${params}`, {
|
|
39
|
+
headers,
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const text = await res.text();
|
|
44
|
+
throw new Error(`Unfragile API error ${res.status}: ${text}`);
|
|
45
|
+
}
|
|
46
|
+
const contentType = res.headers.get("content-type") || "";
|
|
47
|
+
if (!contentType.includes("application/json")) {
|
|
48
|
+
throw new Error(`Unfragile API returned ${contentType} instead of JSON. The API may be down or returning an error page.`);
|
|
49
|
+
}
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ─── Formatters ──────────────────────────────────────────────
|
|
57
|
+
function formatMatch(m, rank) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
const verified = m.artifact.verified ? " ✓" : "";
|
|
60
|
+
const pricing = m.artifact.pricing.free ? "Free" : m.artifact.pricing.model;
|
|
61
|
+
lines.push(`### ${rank}. ${m.artifact.name}${verified}`);
|
|
62
|
+
lines.push(`**Type:** ${m.artifact.type} | **Score:** ${m.compositeScore}/100 | **Rank:** ${m.artifact.unfragileRank}/100 | **Pricing:** ${pricing}`);
|
|
63
|
+
lines.push(`**URL:** ${m.artifact.url}`);
|
|
64
|
+
if (m.artifact.description)
|
|
65
|
+
lines.push(`\n${m.artifact.description}`);
|
|
66
|
+
if (m.capabilities.length > 0) {
|
|
67
|
+
lines.push("\n**Matched Capabilities:**");
|
|
68
|
+
for (const cap of m.capabilities) {
|
|
69
|
+
lines.push(`- **${cap.name}** (${Math.round(cap.matchScore * 100)}% match)`);
|
|
70
|
+
if (cap.description)
|
|
71
|
+
lines.push(` ${cap.description.slice(0, 200)}`);
|
|
72
|
+
if (cap.bestFor.length > 0)
|
|
73
|
+
lines.push(` Best for: ${cap.bestFor.join(", ")}`);
|
|
74
|
+
if (cap.limitations.length > 0)
|
|
75
|
+
lines.push(` Limitations: ${cap.limitations.join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (m.matchGraph.timesMatched > 0) {
|
|
79
|
+
lines.push(`\n**Graph Signal:** Matched ${m.matchGraph.timesMatched} times | ${Math.round(m.matchGraph.successRate * 100)}% success`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`\n→ Details: ${m.artifact.pageUrl}`);
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
function formatResults(data) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`# Search: "${data.query}"`);
|
|
87
|
+
lines.push(`Intent: ${data.intent.type} | Category: ${data.intent.category || "general"}`);
|
|
88
|
+
lines.push(`Found: ${data.matchCount} matches\n`);
|
|
89
|
+
if (data.matches.length === 0) {
|
|
90
|
+
lines.push("No matches found. This gap has been recorded — the Unfragile graph learns from every query.");
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < data.matches.length; i++) {
|
|
94
|
+
lines.push(formatMatch(data.matches[i], i + 1));
|
|
95
|
+
lines.push("");
|
|
96
|
+
}
|
|
97
|
+
// Include matchRecordIds for feedback
|
|
98
|
+
const ids = data.graphSignal.matchRecordIds;
|
|
99
|
+
if (ids && ids.length > 0) {
|
|
100
|
+
lines.push(`\n_Match record IDs (for feedback): ${ids.join(", ")}_`);
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
// ─── MCP Server ──────────────────────────────────────────────
|
|
105
|
+
const server = new McpServer({
|
|
106
|
+
name: "unfragile",
|
|
107
|
+
version: "0.1.0",
|
|
108
|
+
});
|
|
109
|
+
// Tool 1: General search
|
|
110
|
+
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.", {
|
|
111
|
+
query: z.string().min(2).max(500).describe("What you're looking for (e.g., 'best framework for building AI agents', 'MCP server for database access')"),
|
|
112
|
+
limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
|
|
113
|
+
type: z.enum(["agent", "api", "app", "benchmark", "cli", "dataset", "extension", "finetune", "framework", "mcp", "model", "platform", "product", "prompt", "repo", "skill", "template", "webapp", "workflow"]).optional().describe("Filter by artifact type"),
|
|
114
|
+
}, async ({ query, limit, type }) => {
|
|
115
|
+
log("search", query);
|
|
116
|
+
try {
|
|
117
|
+
const data = await searchAPI(query, { limit, type });
|
|
118
|
+
return { content: [{ type: "text", text: formatResults(data) }] };
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Tool 2: Find MCP servers
|
|
125
|
+
server.tool("find_mcps", "Find MCP servers by capability need. Use this when you need to discover MCP servers for specific integrations (e.g., databases, APIs, cloud services). Returns MCP servers ranked by capability match.", {
|
|
126
|
+
need: z.string().min(2).max(500).describe("What capability you need (e.g., 'Postgres database access', 'Slack messaging', 'GitHub repository management')"),
|
|
127
|
+
limit: z.number().min(1).max(20).default(5).describe("Max results"),
|
|
128
|
+
}, async ({ need, limit }) => {
|
|
129
|
+
log("find_mcps", need);
|
|
130
|
+
try {
|
|
131
|
+
const data = await searchAPI(`MCP server for ${need}`, { limit, type: "mcp" });
|
|
132
|
+
const text = formatResults(data);
|
|
133
|
+
return { content: [{ type: "text", text }] };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// Tool 3: Get artifact details
|
|
140
|
+
server.tool("get_artifact", "Get full details and capabilities for a specific AI artifact by name or slug. Uses search-based lookup (best-effort name matching — may return a different artifact for ambiguous names like 'express'). Use this to understand what an artifact can do before adding it to your stack.", {
|
|
141
|
+
name: z.string().min(1).max(200).describe("Artifact name or slug (e.g., 'cursor', 'langchain', 'claude-code')"),
|
|
142
|
+
}, async ({ name }) => {
|
|
143
|
+
log("get_artifact", name);
|
|
144
|
+
try {
|
|
145
|
+
const data = await searchAPI(name, { limit: 3 });
|
|
146
|
+
// Find best match by name
|
|
147
|
+
const match = data.matches.find((m) => m.artifact.name.toLowerCase() === name.toLowerCase() ||
|
|
148
|
+
m.artifact.slug === name.toLowerCase().replace(/\s+/g, "-")) || data.matches[0];
|
|
149
|
+
if (!match) {
|
|
150
|
+
return { content: [{ type: "text", text: `No artifact found matching "${name}".` }] };
|
|
151
|
+
}
|
|
152
|
+
const lines = [];
|
|
153
|
+
lines.push(`# ${match.artifact.name}`);
|
|
154
|
+
lines.push(`**Type:** ${match.artifact.type} | **UnfragileRank:** ${match.artifact.unfragileRank}/100`);
|
|
155
|
+
lines.push(`**URL:** ${match.artifact.url}`);
|
|
156
|
+
lines.push(`**Verified:** ${match.artifact.verified ? "Yes ✓" : "No"}`);
|
|
157
|
+
lines.push(`**Pricing:** ${match.artifact.pricing.free ? "Free" : match.artifact.pricing.model}`);
|
|
158
|
+
lines.push(`**Categories:** ${match.artifact.categories.join(", ")}`);
|
|
159
|
+
if (match.artifact.description)
|
|
160
|
+
lines.push(`\n${match.artifact.description}`);
|
|
161
|
+
if (match.capabilities.length > 0) {
|
|
162
|
+
lines.push("\n## Capabilities");
|
|
163
|
+
for (const cap of match.capabilities) {
|
|
164
|
+
lines.push(`\n### ${cap.name}`);
|
|
165
|
+
if (cap.description)
|
|
166
|
+
lines.push(cap.description);
|
|
167
|
+
if (cap.bestFor.length > 0)
|
|
168
|
+
lines.push(`\n**Best for:** ${cap.bestFor.join(", ")}`);
|
|
169
|
+
if (cap.limitations.length > 0)
|
|
170
|
+
lines.push(`**Limitations:** ${cap.limitations.join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (match.matchGraph.timesMatched > 0) {
|
|
174
|
+
lines.push(`\n## Graph Signal`);
|
|
175
|
+
lines.push(`Matched ${match.matchGraph.timesMatched} times | ${Math.round(match.matchGraph.successRate * 100)}% success rate`);
|
|
176
|
+
if (match.matchGraph.topIntents.length > 0) {
|
|
177
|
+
lines.push(`Top intents: ${match.matchGraph.topIntents.join(", ")}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
lines.push(`\n→ Full details: ${match.artifact.pageUrl}`);
|
|
181
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Tool 4: Compare artifacts
|
|
188
|
+
server.tool("compare", "Compare two AI artifacts side-by-side. Shows capabilities, pricing, rank, and graph signals for each. Uses search-based lookup (best-effort name matching). Use this when deciding between alternatives.", {
|
|
189
|
+
artifact_a: z.string().min(1).max(200).describe("First artifact name (e.g., 'cursor')"),
|
|
190
|
+
artifact_b: z.string().min(1).max(200).describe("Second artifact name (e.g., 'windsurf')"),
|
|
191
|
+
}, async ({ artifact_a, artifact_b }) => {
|
|
192
|
+
log("compare", `${artifact_a} vs ${artifact_b}`);
|
|
193
|
+
try {
|
|
194
|
+
const [dataA, dataB] = await Promise.all([
|
|
195
|
+
searchAPI(artifact_a, { limit: 1 }),
|
|
196
|
+
searchAPI(artifact_b, { limit: 1 }),
|
|
197
|
+
]);
|
|
198
|
+
const matchA = dataA.matches[0];
|
|
199
|
+
const matchB = dataB.matches[0];
|
|
200
|
+
if (!matchA && !matchB) {
|
|
201
|
+
return { content: [{ type: "text", text: `Neither "${artifact_a}" nor "${artifact_b}" found.` }] };
|
|
202
|
+
}
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push(`# Compare: ${matchA?.artifact.name || artifact_a} vs ${matchB?.artifact.name || artifact_b}\n`);
|
|
205
|
+
const row = (label, a, b) => `| ${label} | ${a} | ${b} |`;
|
|
206
|
+
lines.push(`| | ${matchA?.artifact.name || "Not found"} | ${matchB?.artifact.name || "Not found"} |`);
|
|
207
|
+
lines.push(`|---|---|---|`);
|
|
208
|
+
if (matchA && matchB) {
|
|
209
|
+
lines.push(row("Type", matchA.artifact.type, matchB.artifact.type));
|
|
210
|
+
lines.push(row("UnfragileRank", `${matchA.artifact.unfragileRank}/100`, `${matchB.artifact.unfragileRank}/100`));
|
|
211
|
+
lines.push(row("Pricing", matchA.artifact.pricing.free ? "Free" : matchA.artifact.pricing.model, matchB.artifact.pricing.free ? "Free" : matchB.artifact.pricing.model));
|
|
212
|
+
lines.push(row("Verified", matchA.artifact.verified ? "✓" : "No", matchB.artifact.verified ? "✓" : "No"));
|
|
213
|
+
lines.push(row("Times Matched", String(matchA.matchGraph.timesMatched), String(matchB.matchGraph.timesMatched)));
|
|
214
|
+
lines.push(row("Capabilities", String(matchA.capabilities.length), String(matchB.capabilities.length)));
|
|
215
|
+
lines.push(`\n## ${matchA.artifact.name} Capabilities`);
|
|
216
|
+
for (const cap of matchA.capabilities) {
|
|
217
|
+
lines.push(`- **${cap.name}**: ${cap.description.slice(0, 150)}`);
|
|
218
|
+
}
|
|
219
|
+
lines.push(`\n## ${matchB.artifact.name} Capabilities`);
|
|
220
|
+
for (const cap of matchB.capabilities) {
|
|
221
|
+
lines.push(`- **${cap.name}**: ${cap.description.slice(0, 150)}`);
|
|
222
|
+
}
|
|
223
|
+
lines.push(`\n→ Full comparison: https://unfragile.ai/compare/${matchA.artifact.slug}-vs-${matchB.artifact.slug}`);
|
|
224
|
+
}
|
|
225
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// Tool 5: Find harness stack
|
|
232
|
+
server.tool("find_stack", "Assemble a complete AI harness stack for a use case. Given a description of what you're building, returns recommended tools across harness layers: orchestration, tools/MCPs, memory, guardrails, context assembly, and evaluation. This is the key differentiator — Unfragile understands that modern AI systems are composed of 5-15 tools working together.", {
|
|
233
|
+
description: z.string().min(10).max(1000).describe("What you're building (e.g., 'a customer support agent that connects to our Postgres database and Slack, with memory of past conversations')"),
|
|
234
|
+
focus: z.enum(["full", "tools-only", "infrastructure"]).default("full").describe("Stack focus: 'full' = all layers, 'tools-only' = just MCPs and integrations, 'infrastructure' = frameworks and platforms"),
|
|
235
|
+
}, async ({ description, focus }) => {
|
|
236
|
+
log("find_stack", description);
|
|
237
|
+
try {
|
|
238
|
+
// Parallel queries across harness layers
|
|
239
|
+
const layers = focus === "tools-only"
|
|
240
|
+
? [
|
|
241
|
+
{ name: "MCP Servers / Tools", query: `MCP server for ${description}`, type: "mcp" },
|
|
242
|
+
{ name: "APIs", query: `API for ${description}`, type: "api" },
|
|
243
|
+
{ name: "Extensions", query: `extension for ${description}`, type: "extension" },
|
|
244
|
+
]
|
|
245
|
+
: focus === "infrastructure"
|
|
246
|
+
? [
|
|
247
|
+
{ name: "Frameworks", query: `framework for ${description}`, type: "framework" },
|
|
248
|
+
{ name: "Platforms", query: `platform for ${description}`, type: "platform" },
|
|
249
|
+
{ name: "CLI Tools", query: `CLI for ${description}`, type: "cli" },
|
|
250
|
+
]
|
|
251
|
+
: [
|
|
252
|
+
{ name: "Orchestration / Framework", query: `agent framework for ${description}`, type: "framework" },
|
|
253
|
+
{ name: "MCP Servers / Tools", query: `MCP server for ${description}`, type: "mcp" },
|
|
254
|
+
{ name: "APIs", query: `API for ${description}`, type: "api" },
|
|
255
|
+
{ name: "Agents", query: `agent for ${description}`, type: "agent" },
|
|
256
|
+
{ name: "CLI Tools", query: `CLI tool for ${description}`, type: "cli" },
|
|
257
|
+
];
|
|
258
|
+
const results = await Promise.all(layers.map(async (layer) => {
|
|
259
|
+
try {
|
|
260
|
+
const data = await searchAPI(layer.query, { limit: 3, type: layer.type });
|
|
261
|
+
return { layer: layer.name, matches: data.matches };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { layer: layer.name, matches: [] };
|
|
265
|
+
}
|
|
266
|
+
}));
|
|
267
|
+
const lines = [];
|
|
268
|
+
lines.push(`# Harness Stack for: ${description}\n`);
|
|
269
|
+
lines.push(`> Every AI system is a harness — the model is just one component.`);
|
|
270
|
+
lines.push(`> Here's a recommended stack assembled from the Unfragile match graph.\n`);
|
|
271
|
+
let totalTools = 0;
|
|
272
|
+
for (const { layer, matches } of results) {
|
|
273
|
+
if (matches.length === 0)
|
|
274
|
+
continue;
|
|
275
|
+
totalTools += matches.length;
|
|
276
|
+
lines.push(`## ${layer}\n`);
|
|
277
|
+
for (const m of matches) {
|
|
278
|
+
const verified = m.artifact.verified ? " ✓" : "";
|
|
279
|
+
const pricing = m.artifact.pricing.free ? "Free" : m.artifact.pricing.model;
|
|
280
|
+
lines.push(`**${m.artifact.name}${verified}** — ${pricing} | Rank: ${m.artifact.unfragileRank}/100`);
|
|
281
|
+
if (m.artifact.description)
|
|
282
|
+
lines.push(`${m.artifact.description.slice(0, 200)}`);
|
|
283
|
+
if (m.capabilities.length > 0) {
|
|
284
|
+
const capNames = m.capabilities.slice(0, 3).map((c) => c.name).join(", ");
|
|
285
|
+
lines.push(`Key capabilities: ${capNames}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push(`→ ${m.artifact.url}\n`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (totalTools === 0) {
|
|
291
|
+
lines.push("No matching tools found for this use case. This gap has been recorded — the Unfragile graph learns from every query.");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
lines.push(`---`);
|
|
295
|
+
lines.push(`*${totalTools} tools across ${results.filter((r) => r.matches.length > 0).length} harness layers. Every query improves the graph.*`);
|
|
296
|
+
lines.push(`*Browse more: https://unfragile.ai/hub*`);
|
|
297
|
+
}
|
|
298
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
// Tool 6: Feedback — close the learning loop
|
|
305
|
+
server.tool("feedback", "Report whether a recommended tool worked or not. This closes the learning loop — the Unfragile graph uses this feedback to improve future recommendations. Call this after trying a tool from search results.", {
|
|
306
|
+
matchRecordId: z.string().min(1).describe("Match record ID from search results (shown at the bottom of search output)"),
|
|
307
|
+
outcome: z.enum(["success", "failure"]).describe("Did the recommended tool work for your use case?"),
|
|
308
|
+
comment: z.string().max(500).optional().describe("Optional: brief note on why it worked or didn't"),
|
|
309
|
+
}, async ({ matchRecordId, outcome, comment }) => {
|
|
310
|
+
log("feedback", `${matchRecordId} → ${outcome}`);
|
|
311
|
+
try {
|
|
312
|
+
const headers = { "Content-Type": "application/json" };
|
|
313
|
+
if (API_KEY)
|
|
314
|
+
headers["X-API-Key"] = API_KEY;
|
|
315
|
+
const body = {
|
|
316
|
+
matchRecordId,
|
|
317
|
+
outcome: outcome === "success" ? "success" : "failure",
|
|
318
|
+
clickedThrough: true,
|
|
319
|
+
source: SOURCE,
|
|
320
|
+
};
|
|
321
|
+
if (comment)
|
|
322
|
+
body.comment = comment;
|
|
323
|
+
const controller = new AbortController();
|
|
324
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch(`${API_BASE}/api/feedback`, {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers,
|
|
329
|
+
body: JSON.stringify(body),
|
|
330
|
+
signal: controller.signal,
|
|
331
|
+
});
|
|
332
|
+
if (!res.ok) {
|
|
333
|
+
const text = await res.text();
|
|
334
|
+
throw new Error(`Feedback API error ${res.status}: ${text}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
clearTimeout(timeout);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
content: [{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: `Feedback recorded: ${outcome}. The Unfragile graph will use this to improve future recommendations. Thank you!`,
|
|
344
|
+
}],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
return { content: [{ type: "text", text: `Error sending feedback: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
// ─── Start ───────────────────────────────────────────────────
|
|
352
|
+
async function main() {
|
|
353
|
+
const transport = new StdioServerTransport();
|
|
354
|
+
await server.connect(transport);
|
|
355
|
+
console.error("[unfragile] MCP server started");
|
|
356
|
+
}
|
|
357
|
+
main().catch((err) => {
|
|
358
|
+
console.error("Fatal:", err);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unfragile/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unfragile MCP Server — query the match graph for AI from any agent. Find AI tools, assemble harness stacks, compare artifacts.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"ai",
|
|
8
|
+
"tools",
|
|
9
|
+
"discovery",
|
|
10
|
+
"unfragile",
|
|
11
|
+
"harness-engineering"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Unfragile <hello@unfragile.ai>",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Savirinc/Unfragile.ai",
|
|
18
|
+
"directory": "mcp-server"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"bin": {
|
|
23
|
+
"unfragile-mcp": "dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
30
|
+
"dev": "tsx src/index.ts",
|
|
31
|
+
"start": "node dist/index.js",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
|
+
"zod": "^3.24.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.6.0",
|
|
40
|
+
"tsx": "^4.19.4",
|
|
41
|
+
"typescript": "^5.7.3"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|