@unfragile/mcp-server 0.4.0 → 0.6.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 +47 -1
- package/dist/guard.d.ts +2 -0
- package/dist/guard.js +172 -0
- package/dist/index.js +174 -4
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -57,8 +57,54 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## Verify before you install (CLI guard)
|
|
61
|
+
|
|
62
|
+
Don't add an MCP server blind. Verify it first — identity, known security incidents, data-access risk, and cross-vendor reputation, in one signed verdict:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Check a target before installing
|
|
66
|
+
npx -y @unfragile/mcp-server verify mcp:@modelcontextprotocol/server-postgres
|
|
67
|
+
|
|
68
|
+
# Guard a real install — runs the command only if the target isn't FLAGGED
|
|
69
|
+
npx -y @unfragile/mcp-server guard -- claude mcp add pg -- npx -y @scope/pg-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `guard` wrapper extracts the install target from your `claude mcp add` / `cursor mcp add` / `npx` command, verifies it, and **blocks on a `flagged` verdict** (known-bad package, exfiltration, typosquat) unless you pass `--force`. Every verdict is Ed25519-signed and audit-logged — defensible evidence for compliance (EU AI Act Annex III logging/oversight).
|
|
73
|
+
|
|
74
|
+
Verdicts: `trusted` · `caution` · `unverified` · `flagged`.
|
|
75
|
+
|
|
60
76
|
## Tools
|
|
61
77
|
|
|
78
|
+
### Verify (the trust resolver)
|
|
79
|
+
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| `unfragile_verify` | **Call BEFORE invoking or installing any tool/MCP server.** Returns a signed verdict (`trusted`/`caution`/`unverified`/`flagged`) from identity + typosquat checks, the MCP CVE/incident registry, structural data-access risk, and cross-vendor match-graph reputation. Do not invoke anything that comes back `flagged`. |
|
|
83
|
+
|
|
84
|
+
### Resolve (the headline call)
|
|
85
|
+
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `unfragile_resolve` | **Sprint 4.0 headline.** Resolve an agent intent into the single best AI artifact to invoke, with an invocation-ready snippet, an Ed25519-signed trust passport, and ranked alternatives. The outside calibration layer for agent tool selection. |
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// From any MCP-compatible agent
|
|
94
|
+
await mcpClient.callTool("unfragile_resolve", {
|
|
95
|
+
intent: "send email from agent",
|
|
96
|
+
context: { language: "typescript", deploymentTarget: "vercel" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Returns the single best match with:
|
|
100
|
+
// - Invocation snippet ready to copy/paste
|
|
101
|
+
// - Ed25519-signed trust passport (offline-verifiable)
|
|
102
|
+
// - Top 3 alternatives with why_alternative
|
|
103
|
+
// - matchConfidence score
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use `unfragile_resolve` when an agent has decided WHAT it wants to do and needs to choose WHICH artifact to call. Use `search` (below) when an agent needs to browse alternatives rather than commit to one.
|
|
107
|
+
|
|
62
108
|
### Discovery and trust (human-readable)
|
|
63
109
|
|
|
64
110
|
| Tool | Description |
|
|
@@ -66,7 +112,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
66
112
|
| `search` | Find AI tools by intent. "best framework for building AI agents" |
|
|
67
113
|
| `find_mcps` | Discover MCP servers by capability. "Postgres + Slack integration" |
|
|
68
114
|
| `get_artifact` | Get full details + capabilities for a specific artifact |
|
|
69
|
-
| `resolve_capability` | Resolve a `capability://...` URI to ranked trusted artifacts |
|
|
115
|
+
| `resolve_capability` | Resolve a `capability://...` URI to ranked trusted artifacts (GET-based, formatted) |
|
|
70
116
|
| `trust_passport` | Get machine-readable trust evidence: capability URIs, permissions, data access risk, failure modes |
|
|
71
117
|
| `compare` | Compare two artifacts side-by-side |
|
|
72
118
|
| `find_stack` | Assemble a complete harness stack for a use case |
|
package/dist/guard.d.ts
ADDED
package/dist/guard.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// `unfragile` — verify-before-invoke CLI guard.
|
|
4
|
+
//
|
|
5
|
+
// The viral dev hook: verify an MCP server / package BEFORE you install it.
|
|
6
|
+
//
|
|
7
|
+
// npx -y @unfragile/mcp-server verify mcp:@scope/server
|
|
8
|
+
// npx -y @unfragile/mcp-server guard -- claude mcp add pg -- npx -y @scope/pg-mcp
|
|
9
|
+
//
|
|
10
|
+
// `verify <target>` → prints the signed verdict; exit 1 if flagged.
|
|
11
|
+
// `guard -- <cmd…>` → extracts the install target from a `claude mcp add`
|
|
12
|
+
// (or any `npx …`) command, verifies it, BLOCKS on
|
|
13
|
+
// `flagged` (unless --force), then runs the real command.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
const API_BASE = process.env.UNFRAGILE_API_URL || "https://unfragile.ai";
|
|
17
|
+
const API_KEY = process.env.UNFRAGILE_API_KEY || "";
|
|
18
|
+
const C = {
|
|
19
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
20
|
+
green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", gray: "\x1b[90m",
|
|
21
|
+
};
|
|
22
|
+
const isTTY = process.stdout.isTTY;
|
|
23
|
+
const paint = (s, ...codes) => (isTTY ? codes.join("") + s + C.reset : s);
|
|
24
|
+
/** Pull the most likely install target out of a wrapped command's args. */
|
|
25
|
+
function extractTarget(args) {
|
|
26
|
+
const joined = args.join(" ");
|
|
27
|
+
// npx [-y] <pkg> (the common MCP-add shape)
|
|
28
|
+
const npx = joined.match(/npx\s+(?:-y\s+|--yes\s+)?(@?[\w./-]+)/);
|
|
29
|
+
if (npx)
|
|
30
|
+
return `npm:${npx[1]}`;
|
|
31
|
+
// uvx / pipx <pkg>
|
|
32
|
+
const uvx = joined.match(/\b(?:uvx|pipx run)\s+(@?[\w./-]+)/);
|
|
33
|
+
if (uvx)
|
|
34
|
+
return `pypi:${uvx[1]}`;
|
|
35
|
+
// an explicit prefixed ref or URL anywhere in the args
|
|
36
|
+
const ref = args.find((a) => /^(mcp|npm|pypi|github|gh):/.test(a) || /^https?:\/\//.test(a));
|
|
37
|
+
if (ref)
|
|
38
|
+
return ref;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
async function callVerify(target, intent, action) {
|
|
42
|
+
const res = await fetch(`${API_BASE}/api/v1/verify`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json", ...(API_KEY ? { "X-API-Key": API_KEY } : {}) },
|
|
45
|
+
body: JSON.stringify({ target, intent, action }),
|
|
46
|
+
signal: AbortSignal.timeout(15000),
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(`HTTP ${res.status}`);
|
|
50
|
+
return (await res.json());
|
|
51
|
+
}
|
|
52
|
+
function printVerdict(target, v) {
|
|
53
|
+
const verdict = (v.verdict || "unverified");
|
|
54
|
+
const badge = verdict === "trusted" ? paint(" TRUSTED ", C.bold, C.green)
|
|
55
|
+
: verdict === "caution" ? paint(" CAUTION ", C.bold, C.yellow)
|
|
56
|
+
: verdict === "flagged" ? paint(" FLAGGED ", C.bold, C.red)
|
|
57
|
+
: paint(" UNVERIFIED ", C.bold, C.gray);
|
|
58
|
+
console.error("");
|
|
59
|
+
console.error(` ${badge} ${paint(target, C.bold)}`);
|
|
60
|
+
if (v.summary)
|
|
61
|
+
console.error(` ${paint(v.summary, C.dim)}`);
|
|
62
|
+
const s = v.safety || {};
|
|
63
|
+
if (s.score != null)
|
|
64
|
+
console.error(` ${paint("safety", C.gray)} ${s.score}/100 · ${s.dataAccessRisk || "?"} data-access risk`);
|
|
65
|
+
if (s.flags?.length)
|
|
66
|
+
console.error(` ${paint("flags", C.gray)} ${paint(s.flags.join(", "), C.yellow)}`);
|
|
67
|
+
const id = v.identity || {};
|
|
68
|
+
if (id.typosquatRisk)
|
|
69
|
+
console.error(` ${paint("⚠ resembles '" + id.typosquatRisk + "' — possible typosquat", C.yellow)}`);
|
|
70
|
+
const r = v.reputation || {};
|
|
71
|
+
if (r.timesInvoked)
|
|
72
|
+
console.error(` ${paint("rep", C.gray)} ${r.timesInvoked} invocations${r.successRate != null ? `, ${Math.round(r.successRate * 100)}% success` : ""} · rank ${r.unfragileRank ?? 0}/100`);
|
|
73
|
+
if (v.signature)
|
|
74
|
+
console.error(` ${paint("signed by " + (v.signedBy || "unfragile.ai") + " (Ed25519)", C.gray)}`);
|
|
75
|
+
if (v.compliance?.evidenceUrl)
|
|
76
|
+
console.error(` ${paint("audit " + v.compliance.evidenceUrl, C.gray)}`);
|
|
77
|
+
console.error("");
|
|
78
|
+
return verdict;
|
|
79
|
+
}
|
|
80
|
+
function help() {
|
|
81
|
+
console.error(`unfragile — verify-before-invoke guard
|
|
82
|
+
|
|
83
|
+
USAGE
|
|
84
|
+
unfragile verify <target> [--intent "..."] [--action read|write|execute]
|
|
85
|
+
unfragile guard [--force] -- <command...>
|
|
86
|
+
|
|
87
|
+
EXAMPLES
|
|
88
|
+
unfragile verify mcp:@modelcontextprotocol/server-postgres
|
|
89
|
+
unfragile verify npm:postmark-mcp
|
|
90
|
+
unfragile guard -- claude mcp add pg -- npx -y @scope/pg-mcp
|
|
91
|
+
unfragile guard -- cursor mcp add ...
|
|
92
|
+
|
|
93
|
+
BEHAVIOR
|
|
94
|
+
Prints a signed verdict (trusted | caution | unverified | flagged).
|
|
95
|
+
'guard' runs your command only if the target is not FLAGGED.
|
|
96
|
+
Use --force to override a flagged target (logged). Exit 1 on flagged/block.
|
|
97
|
+
|
|
98
|
+
ENV
|
|
99
|
+
UNFRAGILE_API_URL (default https://unfragile.ai)
|
|
100
|
+
UNFRAGILE_API_KEY (optional — higher rate limits)`);
|
|
101
|
+
}
|
|
102
|
+
function getFlag(args, name) {
|
|
103
|
+
const i = args.indexOf(name);
|
|
104
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
105
|
+
}
|
|
106
|
+
async function main() {
|
|
107
|
+
const argv = process.argv.slice(2);
|
|
108
|
+
const cmd = argv[0];
|
|
109
|
+
if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
110
|
+
help();
|
|
111
|
+
process.exit(cmd ? 0 : 1);
|
|
112
|
+
}
|
|
113
|
+
if (cmd === "verify") {
|
|
114
|
+
const target = argv[1];
|
|
115
|
+
if (!target || target.startsWith("-")) {
|
|
116
|
+
console.error("error: `unfragile verify <target>` requires a target.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const intent = getFlag(argv, "--intent");
|
|
120
|
+
const action = getFlag(argv, "--action");
|
|
121
|
+
try {
|
|
122
|
+
const v = await callVerify(target, intent, action);
|
|
123
|
+
const verdict = printVerdict(target, v);
|
|
124
|
+
process.exit(verdict === "flagged" ? 1 : 0);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(paint(` verify unavailable (${err instanceof Error ? err.message : String(err)}) — treat as UNVERIFIED.`, C.yellow));
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (cmd === "guard") {
|
|
132
|
+
const force = argv.includes("--force");
|
|
133
|
+
const sep = argv.indexOf("--");
|
|
134
|
+
if (sep < 0 || sep + 1 >= argv.length) {
|
|
135
|
+
console.error("error: `unfragile guard -- <command...>` requires a command after `--`.");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const wrapped = argv.slice(sep + 1);
|
|
139
|
+
const target = extractTarget(wrapped);
|
|
140
|
+
if (!target) {
|
|
141
|
+
console.error(paint(" no install target detected in command — running unguarded.", C.gray));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
try {
|
|
145
|
+
const v = await callVerify(target, undefined, "execute");
|
|
146
|
+
const verdict = printVerdict(target, v);
|
|
147
|
+
if (verdict === "flagged" && !force) {
|
|
148
|
+
console.error(paint(" ✗ BLOCKED — this target is flagged. Re-run with --force to override (logged).", C.bold + C.red));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
if (verdict === "flagged" && force) {
|
|
152
|
+
console.error(paint(" ⚠ proceeding despite FLAGGED verdict (--force).", C.yellow));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.error(paint(` verify unavailable (${err instanceof Error ? err.message : String(err)}) — proceeding unguarded.`, C.yellow));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Pass through to the real command.
|
|
160
|
+
const child = spawn(wrapped[0], wrapped.slice(1), { stdio: "inherit", shell: false });
|
|
161
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
162
|
+
child.on("error", (e) => {
|
|
163
|
+
console.error(`error running command: ${e.message}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.error(`unknown command '${cmd}'.\n`);
|
|
169
|
+
help();
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
main();
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// the graph learns from every interaction.
|
|
8
8
|
//
|
|
9
9
|
// Tools:
|
|
10
|
+
// unfragile_resolve — HEADLINE: Resolve intent → invocation-ready artifact + signed passport (POST /api/v1/resolve)
|
|
10
11
|
// search — Find AI tools by intent/query (with Match Proof)
|
|
11
12
|
// find_mcps — Discover MCP servers by capability need
|
|
12
13
|
// get_artifact — Get full details + capabilities for an artifact
|
|
13
|
-
// resolve_capability — Resolve capability:// URIs to trusted artifacts (formatted)
|
|
14
|
+
// resolve_capability — Resolve capability:// URIs to trusted artifacts (formatted, GET-based)
|
|
14
15
|
// trust_passport — Get machine-readable trust passport for an artifact (formatted)
|
|
15
16
|
// compare — Compare two artifacts side-by-side
|
|
16
17
|
// find_stack — Assemble a complete harness stack for a use case
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
// Protocol-native (raw JSON, Unfragile Capability Protocol v1):
|
|
22
23
|
// unfragile_validate — Validate an unfragile.yml manifest
|
|
23
24
|
// unfragile_passport — Raw trust passport JSON for an artifact
|
|
24
|
-
// unfragile_resolve_capability — Raw resolver JSON for a capability:// URI / intent
|
|
25
|
+
// unfragile_resolve_capability — Raw resolver JSON for a capability:// URI / intent (GET-based)
|
|
25
26
|
// ─────────────────────────────────────────────────────────────
|
|
26
27
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
28
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -113,6 +114,42 @@ async function resolveAPI(capability, options = {}) {
|
|
|
113
114
|
clearTimeout(timeout);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
117
|
+
// POST /api/v1/resolve — the headline DNS-for-agents call.
|
|
118
|
+
// Takes a natural-language intent (or capability:// URI) and returns
|
|
119
|
+
// invocation-ready snippets + signed passports + ranked alternatives.
|
|
120
|
+
async function resolvePostAPI(intent, options = {}) {
|
|
121
|
+
const headers = {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
Accept: "application/json",
|
|
124
|
+
};
|
|
125
|
+
if (API_KEY)
|
|
126
|
+
headers["X-API-Key"] = API_KEY;
|
|
127
|
+
const body = { intent };
|
|
128
|
+
if (options.context)
|
|
129
|
+
body.context = options.context;
|
|
130
|
+
if (options.type)
|
|
131
|
+
body.type = options.type;
|
|
132
|
+
if (options.limit)
|
|
133
|
+
body.limit = options.limit;
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(`${API_BASE}/api/v1/resolve`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers,
|
|
140
|
+
body: JSON.stringify(body),
|
|
141
|
+
signal: controller.signal,
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const text = await res.text();
|
|
145
|
+
throw new Error(`Unfragile resolve POST API error ${res.status}: ${text}`);
|
|
146
|
+
}
|
|
147
|
+
return res.json();
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
116
153
|
// ─── Formatters ──────────────────────────────────────────────
|
|
117
154
|
/** Extract a readable name from a URL when artifact.name is a raw URL */
|
|
118
155
|
function cleanName(name, url) {
|
|
@@ -227,6 +264,58 @@ function formatPassport(data) {
|
|
|
227
264
|
lines.push(`→ Artifact page: ${p.artifact.page_url}`);
|
|
228
265
|
return lines.join("\n");
|
|
229
266
|
}
|
|
267
|
+
function formatResolvePost(data, intent) {
|
|
268
|
+
const lines = [];
|
|
269
|
+
lines.push(`# Resolved for intent: "${intent}"`);
|
|
270
|
+
lines.push(`Resolver: ${data.resolverVersion} | Generated: ${data.resolvedAt}${data.fromCache ? " (cached)" : ""}`);
|
|
271
|
+
if (data.resolved.length === 0) {
|
|
272
|
+
lines.push("\nNo trusted artifact resolved for this intent. This is a demand gap — the Unfragile graph has recorded it.");
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
for (let i = 0; i < data.resolved.length; i++) {
|
|
276
|
+
const r = data.resolved[i];
|
|
277
|
+
const signed = r.trust.signature ? " ✓ cryptographically signed" : "";
|
|
278
|
+
lines.push(`\n## ${i + 1}. ${r.artifact.name}${signed}`);
|
|
279
|
+
lines.push(`**Type:** ${r.artifact.type} | **Match confidence:** ${Math.round(r.matchConfidence * 100)}%`);
|
|
280
|
+
lines.push(`**URL:** ${r.artifact.url}`);
|
|
281
|
+
lines.push(`\n**Capability:** ${r.capability.name}`);
|
|
282
|
+
lines.push(`Intent fit: "${r.capability.intent}"`);
|
|
283
|
+
lines.push(`\n**Invocation (${r.invocation.language}):**`);
|
|
284
|
+
lines.push("```");
|
|
285
|
+
lines.push(r.invocation.snippet);
|
|
286
|
+
lines.push("```");
|
|
287
|
+
if (r.invocation.requires && r.invocation.requires.length > 0) {
|
|
288
|
+
lines.push(`Requires: ${r.invocation.requires.join(", ")}`);
|
|
289
|
+
}
|
|
290
|
+
if (r.trust.verified) {
|
|
291
|
+
lines.push(`\n**Trust:** Verified, Ed25519-signed.`);
|
|
292
|
+
if (r.trust.score !== undefined)
|
|
293
|
+
lines.push(`Trust score: ${r.trust.score}/100`);
|
|
294
|
+
if (r.trust.data_access_risk)
|
|
295
|
+
lines.push(`Data access risk: ${r.trust.data_access_risk}`);
|
|
296
|
+
if (r.trust.observed_outcomes) {
|
|
297
|
+
const o = r.trust.observed_outcomes;
|
|
298
|
+
lines.push(`Observed: ${o.matches} matches, ${Math.round(o.success_rate * 100)}% success`);
|
|
299
|
+
}
|
|
300
|
+
if (r.trust.signedBy && r.trust.signature) {
|
|
301
|
+
lines.push(`Signed by: ${r.trust.signedBy} (sig: ${r.trust.signature.slice(0, 16)}…)`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
lines.push(`\n**Trust:** Unverified — no signed passport. Verify before production use.`);
|
|
306
|
+
}
|
|
307
|
+
if (r.alternatives.length > 0) {
|
|
308
|
+
lines.push(`\n**Alternatives:**`);
|
|
309
|
+
for (const alt of r.alternatives) {
|
|
310
|
+
lines.push(`- ${alt.name} (${alt.slug}) — ${alt.why_alternative}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (r.lastVerified) {
|
|
314
|
+
lines.push(`\nLast verified: ${r.lastVerified}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return lines.join("\n");
|
|
318
|
+
}
|
|
230
319
|
function formatResolve(data) {
|
|
231
320
|
const lines = [];
|
|
232
321
|
lines.push(`# Resolve: ${data.capability}`);
|
|
@@ -262,7 +351,35 @@ function formatResolve(data) {
|
|
|
262
351
|
// ─── MCP Server ──────────────────────────────────────────────
|
|
263
352
|
const server = new McpServer({
|
|
264
353
|
name: "unfragile",
|
|
265
|
-
version: "0.
|
|
354
|
+
version: "0.5.0",
|
|
355
|
+
});
|
|
356
|
+
// Tool 0: Resolve (HEADLINE — DNS for agents, Sprint 4.0)
|
|
357
|
+
//
|
|
358
|
+
// This is the call most agents should make most of the time when
|
|
359
|
+
// they need to choose WHICH artifact to invoke for a task. Wraps
|
|
360
|
+
// POST /api/v1/resolve, returns invocation-ready snippets +
|
|
361
|
+
// Ed25519-signed trust passport + alternatives + match confidence.
|
|
362
|
+
server.tool("unfragile_resolve", "RESOLVE an agent intent into the single best AI artifact to invoke, with an invocation-ready snippet, a cryptographically signed trust passport, and alternatives. This is Unfragile's headline call — the outside calibration layer for agent tool selection. Use this when an agent needs to choose WHICH tool/MCP/API/framework/model to invoke for a task it has decided to do. Returns the safest current option, not a list.", {
|
|
363
|
+
intent: z.string().min(2).max(500).describe("Natural-language intent (e.g., 'send email from agent', 'query Postgres read-only', 'transcribe an audio file'). Can also be a capability:// URI."),
|
|
364
|
+
context: z.record(z.unknown()).optional().describe("Free-form constraints — project size, language, deployment target, anything the resolver should factor in"),
|
|
365
|
+
type: z.enum(["agent", "api", "app", "benchmark", "cli", "dataset", "extension", "finetune", "framework", "mcp", "model", "platform", "product", "prompt", "repo", "skill", "template", "webapp", "workflow"]).optional().describe("Restrict the resolved artifact to a specific type"),
|
|
366
|
+
limit: z.number().min(1).max(10).default(1).describe("Number of resolutions to return (default 1 — use 1 unless you need backup options inline)"),
|
|
367
|
+
raw: z.boolean().default(false).describe("Return raw JSON for programmatic consumption (default false returns human-readable)"),
|
|
368
|
+
}, async ({ intent, context, type, limit, raw }) => {
|
|
369
|
+
log("unfragile_resolve", intent);
|
|
370
|
+
try {
|
|
371
|
+
const data = await resolvePostAPI(intent, { context, type, limit });
|
|
372
|
+
const text = raw
|
|
373
|
+
? JSON.stringify(data, null, 2)
|
|
374
|
+
: formatResolvePost(data, intent);
|
|
375
|
+
return { content: [{ type: "text", text }] };
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: `Error resolving intent: ${err instanceof Error ? err.message : String(err)}` }],
|
|
380
|
+
isError: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
266
383
|
});
|
|
267
384
|
// Tool 1: General search
|
|
268
385
|
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.", {
|
|
@@ -372,6 +489,56 @@ server.tool("trust_passport", "Get the machine-readable trust passport for an AI
|
|
|
372
489
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
373
490
|
}
|
|
374
491
|
});
|
|
492
|
+
// Tool 5b: Verify-before-invoke — the trust resolver
|
|
493
|
+
server.tool("unfragile_verify", "Verify a tool / MCP server / package BEFORE invoking or installing it. Returns a signed verdict: trusted | caution | unverified | flagged. Pass the install target (e.g. 'mcp:@scope/server', 'npm:pkg', a GitHub/URL, or an `npx ...` command). Checks identity + typosquats, known security incidents/CVEs, data-access risk, and cross-vendor reputation. ALWAYS call this before adding an MCP server or running an unfamiliar tool; do NOT invoke anything that comes back 'flagged'.", {
|
|
494
|
+
target: z.string().min(1).max(500).describe("What you're about to invoke/install: 'mcp:@scope/server', 'npm:pkg', 'pypi:pkg', 'github:owner/repo', an https URL, an `npx -y @scope/pkg` command, or a bare slug."),
|
|
495
|
+
intent: z.string().max(300).optional().describe("What you want to do with it (e.g. 'query postgres read-only') — enables fit scoring."),
|
|
496
|
+
action: z.enum(["read", "write", "execute"]).optional().describe("The kind of access you intend to grant — raises the risk bar for write/execute."),
|
|
497
|
+
}, async ({ target, intent, action }) => {
|
|
498
|
+
log("unfragile_verify", target);
|
|
499
|
+
try {
|
|
500
|
+
const res = await fetch(`${API_BASE}/api/v1/verify`, {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: {
|
|
503
|
+
"Content-Type": "application/json",
|
|
504
|
+
...(API_KEY ? { "X-API-Key": API_KEY } : {}),
|
|
505
|
+
},
|
|
506
|
+
body: JSON.stringify({ target, intent, action }),
|
|
507
|
+
signal: AbortSignal.timeout(15000),
|
|
508
|
+
});
|
|
509
|
+
if (!res.ok) {
|
|
510
|
+
return { content: [{ type: "text", text: `Verify failed (HTTP ${res.status}). Treat '${target}' as UNVERIFIED — do not invoke without manual review.` }], isError: true };
|
|
511
|
+
}
|
|
512
|
+
const v = await res.json();
|
|
513
|
+
const icon = v.verdict === "trusted" ? "✅" : v.verdict === "caution" ? "⚠️" : v.verdict === "flagged" ? "🛑" : "❓";
|
|
514
|
+
const lines = [];
|
|
515
|
+
lines.push(`${icon} VERDICT: ${String(v.verdict || "unverified").toUpperCase()}`);
|
|
516
|
+
lines.push(v.summary || "");
|
|
517
|
+
const id = v.identity || {};
|
|
518
|
+
lines.push(`\nIdentity: ${id.matched ? `matched → ${id.canonical}` : "not found in graph"}${id.verifiedBuilder ? " (verified builder)" : ""}${id.typosquatRisk ? ` ⚠️ resembles '${id.typosquatRisk}' — possible typosquat` : ""}`);
|
|
519
|
+
const s = v.safety || {};
|
|
520
|
+
lines.push(`Safety: ${s.score ?? "?"}/100 · ${s.dataAccessRisk || "?"} data-access risk · permissions: ${(s.permissions || []).join(", ") || "unknown"}`);
|
|
521
|
+
if (s.flags && s.flags.length)
|
|
522
|
+
lines.push(`Flags: ${s.flags.join(", ")}`);
|
|
523
|
+
const r = v.reputation || {};
|
|
524
|
+
if (r.timesInvoked)
|
|
525
|
+
lines.push(`Reputation: ${r.timesInvoked} invocations${r.successRate != null ? `, ${Math.round(r.successRate * 100)}% success` : " (no rated history yet)"} · rank ${r.unfragileRank ?? 0}/100`);
|
|
526
|
+
if (v.verdict === "flagged")
|
|
527
|
+
lines.push(`\n🛑 DO NOT INVOKE. ${s.flags?.includes("known-exfiltration") ? "Known data-exfiltration incident." : "Known security issue."} Verify the publisher and pin a known-good version first.`);
|
|
528
|
+
else if (v.verdict === "unverified")
|
|
529
|
+
lines.push(`\n❓ Not attested by Unfragile. No identity/outcome history. Proceed only with manual review.`);
|
|
530
|
+
else if (v.verdict === "caution")
|
|
531
|
+
lines.push(`\n⚠️ Usable but unproven or write/exec-capable. Review permissions before granting access.`);
|
|
532
|
+
if (v.signature)
|
|
533
|
+
lines.push(`\nSigned by ${v.signedBy || "unfragile.ai"} (Ed25519, offline-verifiable). Verify key: ${v._links?.verify_key || `${API_BASE}/api/v1/trust-passport-public-key`}`);
|
|
534
|
+
if (v.compliance?.evidenceUrl)
|
|
535
|
+
lines.push(`Audit evidence: ${v.compliance.evidenceUrl}`);
|
|
536
|
+
return { content: [{ type: "text", text: lines.filter(Boolean).join("\n") }] };
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
return { content: [{ type: "text", text: `Verify error: ${err instanceof Error ? err.message : String(err)}. Treat '${target}' as UNVERIFIED — do not invoke without manual review.` }], isError: true };
|
|
540
|
+
}
|
|
541
|
+
});
|
|
375
542
|
// Tool 6: Compare artifacts
|
|
376
543
|
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.", {
|
|
377
544
|
artifact_a: z.string().min(1).max(200).describe("First artifact name (e.g., 'cursor')"),
|
|
@@ -505,7 +672,10 @@ server.tool("feedback", "Report whether a recommended tool worked or not. This c
|
|
|
505
672
|
headers["X-API-Key"] = API_KEY;
|
|
506
673
|
const body = {
|
|
507
674
|
matchRecordId,
|
|
508
|
-
|
|
675
|
+
// Canonical outcome values are "success" | "fail" (see MatchOutcome in
|
|
676
|
+
// the schema). The tool exposes "failure" to agents for clarity, but we
|
|
677
|
+
// MUST send "fail" — otherwise the route can't lower successRate.
|
|
678
|
+
outcome: outcome === "success" ? "success" : "fail",
|
|
509
679
|
clickedThrough: true,
|
|
510
680
|
source: SOURCE,
|
|
511
681
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unfragile/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "io.github.Savirinc/unfragile",
|
|
5
|
-
"description": "Unfragile MCP Server — agent-native discovery
|
|
5
|
+
"description": "Unfragile MCP Server — verify-before-invoke trust resolver + agent-native discovery and routing for AI artifacts. Implements the Unfragile Capability Protocol v1.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"ai",
|
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
"type": "module",
|
|
27
27
|
"main": "dist/index.js",
|
|
28
28
|
"bin": {
|
|
29
|
-
"unfragile-mcp": "dist/index.js"
|
|
29
|
+
"unfragile-mcp": "dist/index.js",
|
|
30
|
+
"unfragile": "dist/guard.js"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"dist"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
|
-
"build": "tsc && chmod +x dist/index.js",
|
|
36
|
+
"build": "tsc && chmod +x dist/index.js dist/guard.js",
|
|
36
37
|
"dev": "tsx src/index.ts",
|
|
37
38
|
"start": "node dist/index.js",
|
|
38
39
|
"prepublishOnly": "npm run build"
|