@unfragile/mcp-server 0.3.2 → 0.5.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 +37 -1
- package/dist/index.js +238 -11
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -59,12 +59,38 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
59
59
|
|
|
60
60
|
## Tools
|
|
61
61
|
|
|
62
|
+
### Resolve (the headline call)
|
|
63
|
+
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| `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. |
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// From any MCP-compatible agent
|
|
72
|
+
await mcpClient.callTool("unfragile_resolve", {
|
|
73
|
+
intent: "send email from agent",
|
|
74
|
+
context: { language: "typescript", deploymentTarget: "vercel" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Returns the single best match with:
|
|
78
|
+
// - Invocation snippet ready to copy/paste
|
|
79
|
+
// - Ed25519-signed trust passport (offline-verifiable)
|
|
80
|
+
// - Top 3 alternatives with why_alternative
|
|
81
|
+
// - matchConfidence score
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
### Discovery and trust (human-readable)
|
|
87
|
+
|
|
62
88
|
| Tool | Description |
|
|
63
89
|
|------|-------------|
|
|
64
90
|
| `search` | Find AI tools by intent. "best framework for building AI agents" |
|
|
65
91
|
| `find_mcps` | Discover MCP servers by capability. "Postgres + Slack integration" |
|
|
66
92
|
| `get_artifact` | Get full details + capabilities for a specific artifact |
|
|
67
|
-
| `resolve_capability` | Resolve a `capability://...` URI to ranked trusted artifacts |
|
|
93
|
+
| `resolve_capability` | Resolve a `capability://...` URI to ranked trusted artifacts (GET-based, formatted) |
|
|
68
94
|
| `trust_passport` | Get machine-readable trust evidence: capability URIs, permissions, data access risk, failure modes |
|
|
69
95
|
| `compare` | Compare two artifacts side-by-side |
|
|
70
96
|
| `find_stack` | Assemble a complete harness stack for a use case |
|
|
@@ -72,6 +98,16 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
72
98
|
| `subscribe` | Watch for new artifacts matching a capability need |
|
|
73
99
|
| `unsubscribe` | Cancel a monitor |
|
|
74
100
|
|
|
101
|
+
### Capability Protocol v1 (raw JSON, machine-readable)
|
|
102
|
+
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `unfragile_validate` | Validate an `unfragile.yml` manifest against the Capability Protocol v1 schema. Returns errors, warnings, and the parsed manifest. |
|
|
106
|
+
| `unfragile_passport` | Fetch the raw `trust_passport_v1` JSON for an artifact by slug. Suitable for programmatic policy checks. |
|
|
107
|
+
| `unfragile_resolve_capability` | Resolve a `capability://` URI or natural-language intent into ranked artifacts as raw JSON. Optional inline passports for one-call decisioning. |
|
|
108
|
+
|
|
109
|
+
Learn more about the protocol: <https://unfragile.ai/protocol>.
|
|
110
|
+
|
|
75
111
|
## Examples
|
|
76
112
|
|
|
77
113
|
Once installed, ask your agent:
|
package/dist/index.js
CHANGED
|
@@ -7,16 +7,22 @@
|
|
|
7
7
|
// the graph learns from every interaction.
|
|
8
8
|
//
|
|
9
9
|
// Tools:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
10
|
+
// unfragile_resolve — HEADLINE: Resolve intent → invocation-ready artifact + signed passport (POST /api/v1/resolve)
|
|
11
|
+
// search — Find AI tools by intent/query (with Match Proof)
|
|
12
|
+
// find_mcps — Discover MCP servers by capability need
|
|
13
|
+
// get_artifact — Get full details + capabilities for an artifact
|
|
14
|
+
// resolve_capability — Resolve capability:// URIs to trusted artifacts (formatted, GET-based)
|
|
15
|
+
// trust_passport — Get machine-readable trust passport for an artifact (formatted)
|
|
16
|
+
// compare — Compare two artifacts side-by-side
|
|
17
|
+
// find_stack — Assemble a complete harness stack for a use case
|
|
18
|
+
// feedback — Report success/failure to close the learning loop
|
|
19
|
+
// subscribe — Set up persistent watch for new tools
|
|
20
|
+
// unsubscribe — Cancel a persistent watch
|
|
21
|
+
//
|
|
22
|
+
// Protocol-native (raw JSON, Unfragile Capability Protocol v1):
|
|
23
|
+
// unfragile_validate — Validate an unfragile.yml manifest
|
|
24
|
+
// unfragile_passport — Raw trust passport JSON for an artifact
|
|
25
|
+
// unfragile_resolve_capability — Raw resolver JSON for a capability:// URI / intent (GET-based)
|
|
20
26
|
// ─────────────────────────────────────────────────────────────
|
|
21
27
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
28
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -108,6 +114,42 @@ async function resolveAPI(capability, options = {}) {
|
|
|
108
114
|
clearTimeout(timeout);
|
|
109
115
|
}
|
|
110
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
|
+
}
|
|
111
153
|
// ─── Formatters ──────────────────────────────────────────────
|
|
112
154
|
/** Extract a readable name from a URL when artifact.name is a raw URL */
|
|
113
155
|
function cleanName(name, url) {
|
|
@@ -222,6 +264,58 @@ function formatPassport(data) {
|
|
|
222
264
|
lines.push(`→ Artifact page: ${p.artifact.page_url}`);
|
|
223
265
|
return lines.join("\n");
|
|
224
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
|
+
}
|
|
225
319
|
function formatResolve(data) {
|
|
226
320
|
const lines = [];
|
|
227
321
|
lines.push(`# Resolve: ${data.capability}`);
|
|
@@ -257,7 +351,35 @@ function formatResolve(data) {
|
|
|
257
351
|
// ─── MCP Server ──────────────────────────────────────────────
|
|
258
352
|
const server = new McpServer({
|
|
259
353
|
name: "unfragile",
|
|
260
|
-
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
|
+
}
|
|
261
383
|
});
|
|
262
384
|
// Tool 1: General search
|
|
263
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.", {
|
|
@@ -598,6 +720,111 @@ server.tool("subscribe", "Set up a persistent watch for new AI tools matching a
|
|
|
598
720
|
};
|
|
599
721
|
}
|
|
600
722
|
});
|
|
723
|
+
// ─── Protocol-native tools ──────────────────────────────────
|
|
724
|
+
//
|
|
725
|
+
// These three tools mirror the Unfragile Capability Protocol
|
|
726
|
+
// endpoints 1:1 and return raw JSON. Use them when you want
|
|
727
|
+
// machine-readable output rather than the formatted markdown
|
|
728
|
+
// returned by `trust_passport` / `resolve_capability`.
|
|
729
|
+
async function validateManifestAPI(body) {
|
|
730
|
+
const headers = {
|
|
731
|
+
"Content-Type": "application/json",
|
|
732
|
+
Accept: "application/json",
|
|
733
|
+
};
|
|
734
|
+
if (API_KEY)
|
|
735
|
+
headers["X-API-Key"] = API_KEY;
|
|
736
|
+
const controller = new AbortController();
|
|
737
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
738
|
+
try {
|
|
739
|
+
const res = await fetch(`${API_BASE}/api/v1/validate`, {
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers,
|
|
742
|
+
body: JSON.stringify(body),
|
|
743
|
+
signal: controller.signal,
|
|
744
|
+
});
|
|
745
|
+
const text = await res.text();
|
|
746
|
+
try {
|
|
747
|
+
return JSON.parse(text);
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
throw new Error(`Validator returned non-JSON (${res.status}): ${text.slice(0, 300)}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
finally {
|
|
754
|
+
clearTimeout(timeout);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Tool: unfragile_validate
|
|
758
|
+
server.tool("unfragile_validate", "Validate an unfragile.yml manifest against the Unfragile Capability Protocol v1. Returns the structured validation result with errors, warnings, and the parsed manifest. Use this before submitting an artifact or in CI pipelines. Accepts raw YAML text via `yaml_content`.", {
|
|
759
|
+
yaml_content: z
|
|
760
|
+
.string()
|
|
761
|
+
.min(10)
|
|
762
|
+
.max(200_000)
|
|
763
|
+
.describe("Raw contents of the unfragile.yml file"),
|
|
764
|
+
}, async ({ yaml_content }) => {
|
|
765
|
+
log("unfragile_validate", `${yaml_content.length} bytes`);
|
|
766
|
+
try {
|
|
767
|
+
const data = await validateManifestAPI({ yaml: yaml_content });
|
|
768
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{ type: "text", text: `Error validating manifest: ${err instanceof Error ? err.message : String(err)}` },
|
|
774
|
+
],
|
|
775
|
+
isError: true,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
// Tool: unfragile_passport — raw JSON trust passport
|
|
780
|
+
server.tool("unfragile_passport", "Fetch the raw machine-readable trust passport for an artifact by slug (trust_passport_v1 schema). Returns JSON suitable for programmatic policy checks. Pair with `trust_passport` when you want a human-readable summary.", {
|
|
781
|
+
slug: z.string().min(1).max(200).describe("Artifact slug (e.g., 'cursor', 'langchain', 'postgres-mcp-server')"),
|
|
782
|
+
}, async ({ slug }) => {
|
|
783
|
+
log("unfragile_passport", slug);
|
|
784
|
+
try {
|
|
785
|
+
const data = await passportAPI(slug);
|
|
786
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
787
|
+
}
|
|
788
|
+
catch (err) {
|
|
789
|
+
return {
|
|
790
|
+
content: [
|
|
791
|
+
{ type: "text", text: `Error fetching passport: ${err instanceof Error ? err.message : String(err)}` },
|
|
792
|
+
],
|
|
793
|
+
isError: true,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
// Tool: unfragile_resolve_capability — raw JSON resolver
|
|
798
|
+
server.tool("unfragile_resolve_capability", "Resolve a `capability://` URI or a natural-language capability into ranked artifacts, with trust signals and passport URLs. Returns raw JSON so an agent can make a programmatic selection. Pair with `resolve_capability` when you want a human-readable summary.", {
|
|
799
|
+
uri_or_intent: z
|
|
800
|
+
.string()
|
|
801
|
+
.min(2)
|
|
802
|
+
.max(500)
|
|
803
|
+
.describe("Capability URI like `capability://database.postgres.query.readonly`, or a natural-language capability description"),
|
|
804
|
+
limit: z.number().min(1).max(20).default(5).describe("Max resolutions"),
|
|
805
|
+
risk: z
|
|
806
|
+
.enum(["default", "production", "enterprise", "strict"])
|
|
807
|
+
.default("default")
|
|
808
|
+
.describe("Risk posture — stricter modes require stronger quality/status gates"),
|
|
809
|
+
passport: z
|
|
810
|
+
.boolean()
|
|
811
|
+
.default(false)
|
|
812
|
+
.describe("Inline the full trust passport for each resolution (larger response)"),
|
|
813
|
+
}, async ({ uri_or_intent, limit, risk, passport }) => {
|
|
814
|
+
log("unfragile_resolve_capability", uri_or_intent);
|
|
815
|
+
try {
|
|
816
|
+
const data = await resolveAPI(uri_or_intent, { limit, risk, passport });
|
|
817
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
return {
|
|
821
|
+
content: [
|
|
822
|
+
{ type: "text", text: `Error resolving capability: ${err instanceof Error ? err.message : String(err)}` },
|
|
823
|
+
],
|
|
824
|
+
isError: true,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
});
|
|
601
828
|
// Tool 10: Unsubscribe — delete a monitor
|
|
602
829
|
server.tool("unsubscribe", "Cancel a persistent watch (monitor). Use the monitor ID returned from subscribe.", {
|
|
603
830
|
monitorId: z.string().min(1).describe("Monitor ID from subscribe (e.g., 'mon_...')"),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unfragile/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.Savirinc/unfragile",
|
|
5
|
-
"description": "Unfragile MCP Server — agent-native discovery, trust, and routing for AI artifacts.",
|
|
5
|
+
"description": "Unfragile MCP Server — agent-native discovery, trust, and routing for AI artifacts. Implements the Unfragile Capability Protocol v1.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"ai",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"routing",
|
|
13
13
|
"capabilities",
|
|
14
14
|
"unfragile",
|
|
15
|
-
"harness-engineering"
|
|
15
|
+
"harness-engineering",
|
|
16
|
+
"capability-protocol",
|
|
17
|
+
"unfragile.yml"
|
|
16
18
|
],
|
|
17
19
|
"license": "MIT",
|
|
18
20
|
"author": "Unfragile <hello@unfragile.ai>",
|