@forcedream/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ForceDream
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @forcedream/mcp-server
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for **ForceDream** — discover AI agents, invoke them to do real work, and **verify the result cryptographically in your own process**. No trust in ForceDream required: every agent task produces an Ed25519-signed proof that this server checks locally.
4
+
5
+ ## What it does
6
+
7
+ Three tools, exposed to any MCP client (Claude Desktop, Cursor, Cline, …):
8
+
9
+ | Tool | Auth | What it does |
10
+ |------|------|--------------|
11
+ | `search_agents` | none | Discover ForceDream agents and their honest, system-derived metrics (proof count, success rate). |
12
+ | `verify_proof` | none | Independently verify a ForceDream proof by task ID. The Ed25519 signature is checked locally against the published public key — ForceDream is never asked whether the proof is valid. |
13
+ | `invoke_agent` | key | Invoke an agent to do real work. Spends your balance. Returns the output plus a `proof_id` you can verify. |
14
+
15
+ `search_agents` and `verify_proof` need no account. `invoke_agent` spends a balance, so it needs your key.
16
+
17
+ ## Quick start
18
+
19
+ ### 1. Get a key
20
+
21
+ Sign up at [forcedream.com](https://www.forcedream.com/earn). You'll receive a billing key (`fd_live_…`) and a small **trial balance**, so you can invoke an agent immediately — no payment required to try it.
22
+
23
+ ### 2. Add to Claude Desktop
24
+
25
+ Edit your `claude_desktop_config.json`:
26
+
27
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
28
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "forcedream": {
34
+ "command": "npx",
35
+ "args": ["-y", "@forcedream/mcp-server"],
36
+ "env": {
37
+ "FD_API_KEY": "fd_live_your_key_here"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Restart Claude Desktop. You should see the three ForceDream tools available.
45
+
46
+ > Omit `FD_API_KEY` to run discovery + verification only (no spending). Add it to enable `invoke_agent`.
47
+
48
+ ### 3. Try it
49
+
50
+ In a new chat:
51
+
52
+ > "Search the ForceDream agents, then invoke data-extract-v1 to pull the year from 'founded in 1998', then verify the proof it returns."
53
+
54
+ You'll watch discovery → invocation → trustless verification, end to end.
55
+
56
+ ## Configuration
57
+
58
+ | Env var | Required | Default | Purpose |
59
+ |---------|----------|---------|---------|
60
+ | `FD_API_KEY` | only for `invoke_agent` | — | Your `fd_live_` billing key. Spending happens against its balance. |
61
+ | `FD_API_BASE` | no | `https://api.forcedream.ai` | Override the API base (for testing). |
62
+
63
+ ## What a proof proves — and what it doesn't
64
+
65
+ A valid proof attests **provenance and integrity**: that ForceDream produced this exact output for this exact input, at this cost, and that nothing has been altered since. The signature is checked in your process, so you don't have to trust ForceDream's word.
66
+
67
+ A proof does **not** attest factual correctness. An agent's answer can still be wrong; the proof only guarantees it is the agent's genuine, unmodified work. Verify cited sources yourself.
68
+
69
+ You can also verify any proof in a browser at [forcedream.com/proof](https://www.forcedream.com/proof).
70
+
71
+ ## Run it directly
72
+
73
+ ```bash
74
+ npx -y @forcedream/mcp-server # starts the stdio MCP server
75
+ ```
76
+
77
+ It speaks MCP over stdio; point any MCP client at it.
78
+
79
+ ## Links
80
+
81
+ - ForceDream: https://www.forcedream.com
82
+ - Verify a proof: https://www.forcedream.com/proof
83
+ - MCP: https://modelcontextprotocol.io
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+ // EXACT replica of the server's wfCanonical: JSON.stringify(obj, Object.keys(obj).sort())
3
+ // Replacer-ARRAY form (emits only listed keys, sorted). NOT a .reduce() lookalike.
4
+ // Proven against production proof wtask_a3ff3b4f3a1f6bf7df1f -> SIGNATURE VALID: true.
5
+ export function wfCanonical(obj) {
6
+ return JSON.stringify(obj, Object.keys(obj).sort());
7
+ }
8
+ export function sha256hex(s) {
9
+ return createHash('sha256').update(s).digest('hex');
10
+ }
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { verifyProof } from './verify_proof.js';
6
+ import { searchAgents, searchAgentsSchema } from './search_agents.js';
7
+ import { invokeAgent, invokeAgentSchema } from './invoke_agent.js';
8
+ const server = new McpServer({ name: 'forcedream', version: '0.1.0' });
9
+ // verify_proof — trustless, keyless. Verify a ForceDream proof's Ed25519 signature client-side.
10
+ server.registerTool('verify_proof', {
11
+ title: 'Verify a ForceDream proof',
12
+ description: 'Independently verify that a ForceDream agent proof is authentic and untampered, using public-key cryptography. ' +
13
+ 'Provide a task_id (proof is fetched from the public endpoint) or a full proof object. Verification runs locally — ' +
14
+ 'ForceDream is never asked whether the proof is valid; the Ed25519 math decides. No account or key needed.',
15
+ inputSchema: {
16
+ task_id: z.string().optional().describe('The ForceDream task ID whose proof to verify (e.g. wtask_...).'),
17
+ proof: z.record(z.any()).optional().describe('Optional: a full proof object to verify directly (skips the fetch).'),
18
+ },
19
+ }, async ({ task_id, proof }) => {
20
+ try {
21
+ const result = await verifyProof({ task_id, proof: proof });
22
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
23
+ }
24
+ catch (e) {
25
+ return { content: [{ type: 'text', text: JSON.stringify({ verified: false, error: e.message }, null, 2) }], isError: true };
26
+ }
27
+ });
28
+ // search_agents — keyless discovery of real agents with honest, system-derived metrics.
29
+ server.registerTool('search_agents', {
30
+ title: 'Search ForceDream agents',
31
+ description: 'Discover ForceDream agents and their honest, system-derived metrics (proof_count, success_rate). ' +
32
+ 'Optionally filter by capability (e.g. "research:citation") or free-text query. No key needed. ' +
33
+ 'Every agent listed has real cryptographic proofs you can verify with verify_proof.',
34
+ inputSchema: searchAgentsSchema,
35
+ }, async ({ capability, query }) => {
36
+ try {
37
+ const result = await searchAgents({ capability, query });
38
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
39
+ }
40
+ catch (e) {
41
+ return { content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
42
+ }
43
+ });
44
+ // invoke_agent — spends balance (needs FD_API_KEY). Returns output + a verifiable proof_id.
45
+ server.registerTool('invoke_agent', {
46
+ title: 'Invoke a ForceDream agent',
47
+ description: 'Invoke a ForceDream agent to do real work. SPENDS your balance — requires FD_API_KEY in the server env. ' +
48
+ 'Returns the output, what you were charged, and a proof_id you can verify with verify_proof. ' +
49
+ 'Handles honest declines (charged 0) and insufficient balance gracefully. Invokes once; never double-charges.',
50
+ inputSchema: invokeAgentSchema,
51
+ }, async ({ agent_slug, task, max_wait_seconds }) => {
52
+ try {
53
+ const result = await invokeAgent({ agent_slug, task, max_wait_seconds });
54
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
55
+ }
56
+ catch (e) {
57
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
58
+ }
59
+ });
60
+ const transport = new StdioServerTransport();
61
+ await server.connect(transport);
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
3
+ export const invokeAgentSchema = {
4
+ agent_slug: z.string().describe('The agent to invoke, e.g. "atlas-research-v1". Use search_agents to discover.'),
5
+ task: z.string().describe('The task/query for the agent (Atlas: a research question).'),
6
+ max_wait_seconds: z.number().optional().describe('Max seconds to poll (default 60). On timeout, returns task_id to poll later.'),
7
+ };
8
+ function authHeader() {
9
+ const key = process.env.FD_API_KEY || '';
10
+ return key ? { Authorization: `Bearer ${key}` } : {};
11
+ }
12
+ async function postJson(url, body) {
13
+ const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader() }, body: JSON.stringify(body) });
14
+ let json = null;
15
+ try {
16
+ json = await res.json();
17
+ }
18
+ catch { }
19
+ return { status: res.status, json };
20
+ }
21
+ async function getJson(url) {
22
+ const res = await fetch(url, { headers: authHeader() });
23
+ let json = null;
24
+ try {
25
+ json = await res.json();
26
+ }
27
+ catch { }
28
+ return { status: res.status, json };
29
+ }
30
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
+ // Invoke a ForceDream agent and wait (bounded) for the result. SPENDS balance — needs FD_API_KEY.
32
+ // Invokes ONCE; on timeout does NOT re-invoke (would double-charge), returns task_id instead.
33
+ export async function invokeAgent(args) {
34
+ if (!process.env.FD_API_KEY) {
35
+ return { status: 'error', agent: args.agent_slug, message: 'FD_API_KEY is required to invoke (invoking spends your balance). Set it in the MCP server env. search_agents and verify_proof need no key.' };
36
+ }
37
+ const slug = args.agent_slug;
38
+ const maxWaitMs = Math.max(5, Math.min(120, args.max_wait_seconds ?? 60)) * 1000;
39
+ const inv = await postJson(`${FD_API}/v1/agents/${encodeURIComponent(slug)}/invoke`, { task: args.task });
40
+ if (inv.status === 401)
41
+ return { status: 'error', agent: slug, message: 'Invalid FD_API_KEY (401). Check the key in the MCP server env.' };
42
+ if (!inv.json?.task_id)
43
+ return { status: 'error', agent: slug, message: `Invoke failed (HTTP ${inv.status}): ${inv.json?.error || inv.json?.note || 'no task_id'}` };
44
+ const taskId = inv.json.task_id;
45
+ const start = Date.now();
46
+ let intervalMs = 2500;
47
+ while (Date.now() - start < maxWaitMs) {
48
+ await sleep(intervalMs);
49
+ const poll = await getJson(`${FD_API}/v1/agents/${encodeURIComponent(slug)}/result/${encodeURIComponent(taskId)}`);
50
+ const d = poll.json || {};
51
+ const status = d.status || d.outcome;
52
+ if (status === 'completed' || status === 'succeeded' || d.ok === true) {
53
+ if (d.outcome === 'insufficient' || (d.output && d.output.confidence === 'insufficient')) {
54
+ return { status: 'insufficient', agent: slug, task_id: taskId, output: d.output, charged_pence: 0, message: 'Agent returned insufficient evidence and declined rather than fabricate. Charged nothing.' };
55
+ }
56
+ return {
57
+ status: 'completed', agent: slug, task_id: taskId, output: d.output, charged_pence: d.charged_pence,
58
+ proof_id: d.proof_id || taskId,
59
+ verify_proof_hint: `Verify trustlessly: call verify_proof with task_id "${d.proof_id || taskId}". The signature proves authenticity without trusting ForceDream.`,
60
+ message: `Completed. Charged ${d.charged_pence}p. Cryptographically proven (proof_id ${d.proof_id || taskId}).`,
61
+ };
62
+ }
63
+ if (status === 'insufficient')
64
+ return { status: 'insufficient', agent: slug, task_id: taskId, output: d.output, charged_pence: 0, message: 'Agent declined (insufficient evidence). Charged nothing.' };
65
+ if (status === 'charge_failed')
66
+ return { status: 'charge_failed', agent: slug, task_id: taskId, charged_pence: 0, message: `Charge failed: ${d.reason || 'insufficient_balance'}. Nothing charged or delivered. Top up and retry.` };
67
+ if (status === 'failed' || status === 'dead_letter')
68
+ return { status: 'error', agent: slug, task_id: taskId, message: `Task ${status}: ${d.reason || d.last_error || 'unknown'}` };
69
+ intervalMs = Math.min(intervalMs + 1000, 6000);
70
+ }
71
+ return { status: 'pending', agent: slug, task_id: taskId, message: `Still processing after ${maxWaitMs / 1000}s. Not re-invoked (would double-charge). Poll the result later with this task_id.` };
72
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
3
+ export const searchAgentsSchema = {
4
+ capability: z.string().optional().describe('Optional capability filter (e.g. "research:citation"). Omit to return all.'),
5
+ query: z.string().optional().describe('Optional free-text match against agent slug/name/capability.'),
6
+ };
7
+ async function fetchJson(url) {
8
+ const res = await fetch(url);
9
+ if (!res.ok)
10
+ throw new Error(`fetch ${url} -> HTTP ${res.status}`);
11
+ return res.json();
12
+ }
13
+ // Discover ForceDream agents. Keyless (public registry). Honest system-derived metrics only.
14
+ // Server has no working capability filter -> filter client-side.
15
+ export async function searchAgents(args) {
16
+ const data = await fetchJson(`${FD_API}/v1/agents/list`);
17
+ let agents = Array.isArray(data?.agents) ? data.agents : [];
18
+ if (args.capability) {
19
+ const cap = args.capability.toLowerCase();
20
+ agents = agents.filter((a) => (a.capabilities || []).some((c) => c.toLowerCase() === cap));
21
+ }
22
+ if (args.query) {
23
+ const q = args.query.toLowerCase();
24
+ agents = agents.filter((a) => a.slug.toLowerCase().includes(q) ||
25
+ (a.name || '').toLowerCase().includes(q) ||
26
+ (a.capabilities || []).some((c) => c.toLowerCase().includes(q)));
27
+ }
28
+ const enriched = agents.map((a) => ({
29
+ ...a,
30
+ verify_proofs: `${FD_API}/v1/agents/${a.slug}/proofs`,
31
+ invoke: `${FD_API}/v1/agents/${a.slug}/invoke`,
32
+ }));
33
+ return {
34
+ count: enriched.length,
35
+ agents: enriched,
36
+ note: enriched.length === 0
37
+ ? 'No agents matched. The registry contains only real, registered agents with cryptographic proofs.'
38
+ : 'Metrics are system-derived from proofs/ledger (proof_count, success_rate) — never self-reported. Each agent\'s proofs are independently verifiable.',
39
+ };
40
+ }
@@ -0,0 +1,77 @@
1
+ import { generateKeyPairSync, sign as edSign } from 'node:crypto';
2
+ import { verifyProof } from './verify_proof.js';
3
+ import { wfCanonical, sha256hex } from './canonical.js';
4
+ // New 10-field proof (Atlas) — proven genuine.
5
+ const newProof = {
6
+ proof_id: 'wfproof_519150efb3b4d99c88dc',
7
+ task_id: 'wtask_a3ff3b4f3a1f6bf7df1f',
8
+ agent_id: 'atlas-research-v1',
9
+ input_hash: '67c25de0835136e1034351b999207b2f5dbe016b0b076464e7e4dae29f1a8d51',
10
+ output_hash: 'e601679cbd5cdcfd3e56e9f618feea64a8ebd80b5dcf50781cec095f095c1771',
11
+ cost_pence: 25,
12
+ budget_pence: 800,
13
+ external_cost_hash: '823e53d3be06e23d653bb015191a0c37fedbb9c7343200e293aeefbeb5096de4',
14
+ retrieved_count: 6,
15
+ started_at: 1782479438487,
16
+ completed_at: '1782479446000',
17
+ algorithm: 'Ed25519',
18
+ signature: 'mOIqV583PmDPDHK7K854O6e/Y/mOmAUc4z8DZbt55Dvcx7iFcYflBGx3BWnq3pExSvT5RgFYlByHj8mbg08MBA==',
19
+ };
20
+ // Old 8-field proof (compliance, pre-external-cost) — proven genuine.
21
+ const oldProof = {
22
+ proof_id: 'wfproof_ee150e758c537ffa5ce6',
23
+ task_id: 'wtask_f698bc60ebe2a58c5f7c',
24
+ agent_id: 'dispatcher',
25
+ input_hash: 'f104d3d92ef7f469be33aef5b0eb843b7fe22f8e84071f236e6dfbef6d46fcc7',
26
+ output_hash: '8ef1e01c13b5e611da11efca28af6a6d77db3f1ea7a9792dc4a21a15b0e11b96',
27
+ cost_pence: 27,
28
+ budget_pence: 1500,
29
+ started_at: 1782421109155,
30
+ completed_at: '1782421120600',
31
+ algorithm: 'Ed25519',
32
+ signature: '4VTrOX/3+1hdXhSKCZiUNxILJUT03VQ238505qIvszlOhbmpKTiqW7h1fsvwBOiO4gZ4q8ycJgeljmN6F/5zBg==',
33
+ };
34
+ function row(n, got, want) {
35
+ const ok = got === want;
36
+ console.log(`[${n}] -> verified=${got} ${ok ? 'PASS' : 'FAIL'}`);
37
+ return ok;
38
+ }
39
+ async function main() {
40
+ let pass = true;
41
+ pass = row('1 genuine NEW (10-field)', (await verifyProof({ proof: newProof })).verified, true) && pass;
42
+ pass = row('2 tampered cost ', (await verifyProof({ proof: { ...newProof, cost_pence: 9999 } })).verified, false) && pass;
43
+ pass = row('3 tampered output ', (await verifyProof({ proof: { ...newProof, output_hash: '0'.repeat(64) } })).verified, false) && pass;
44
+ pass = row('4 tampered extcost ', (await verifyProof({ proof: { ...newProof, external_cost_hash: '0'.repeat(64) } })).verified, false) && pass;
45
+ // 6: genuine OLD 8-field proof MUST verify (regression for historical proofs)
46
+ const old = await verifyProof({ proof: oldProof });
47
+ pass = row(`6 genuine OLD (${old.fields_signed}-field) `, old.verified, true) && pass;
48
+ // 7: missing signature -> false, no throw
49
+ const { signature, ...noSig } = newProof;
50
+ pass = row('7 missing signature ', (await verifyProof({ proof: noSig })).verified, false) && pass;
51
+ // 8: wrong algorithm -> false
52
+ pass = row('8 wrong algorithm ', (await verifyProof({ proof: { ...newProof, algorithm: 'RS256' } })).verified, false) && pass;
53
+ // 9: garbage proof -> graceful false
54
+ try {
55
+ const g = await verifyProof({ proof: { task_id: 'x', agent_id: 'y' } });
56
+ pass = row('9 garbage proof ', g.verified, false) && pass;
57
+ }
58
+ catch (e) {
59
+ console.log('[9] garbage proof -> THREW (should be graceful false):', e.message, 'FAIL');
60
+ pass = false;
61
+ }
62
+ // 10: proof signed by a DIFFERENT key must be rejected (we verify the REAL key, not any key)
63
+ const { privateKey } = generateKeyPairSync('ed25519');
64
+ const forged = { ...newProof };
65
+ const signable = {
66
+ task_id: forged.task_id, agent_id: forged.agent_id, input_hash: forged.input_hash, output_hash: forged.output_hash,
67
+ cost_pence: Number(forged.cost_pence), budget_pence: Number(forged.budget_pence),
68
+ external_cost_hash: String(forged.external_cost_hash), retrieved_count: Number(forged.retrieved_count),
69
+ started_at: Number(forged.started_at), completed_at: String(forged.completed_at),
70
+ };
71
+ const digest = sha256hex(wfCanonical(signable));
72
+ forged.signature = edSign(null, Buffer.from(digest, 'hex'), privateKey).toString('base64');
73
+ pass = row('10 wrong-key forgery ', (await verifyProof({ proof: forged })).verified, false) && pass;
74
+ console.log('\n=== SELF-TEST:', pass ? 'ALL PASS' : 'FAILURES', '===');
75
+ process.exit(pass ? 0 : 1);
76
+ }
77
+ main();
@@ -0,0 +1,71 @@
1
+ import { createPublicKey, verify as edVerify } from 'node:crypto';
2
+ import { wfCanonical, sha256hex } from './canonical.js';
3
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
4
+ // Reconstruct the signable EXACTLY as wfGenerateProof did. Version-aware:
5
+ // proofs with external_cost_hash were signed over 10 fields, older ones over 8.
6
+ // Types matter (wfCanonical stringifies 5 vs "5" differently):
7
+ // cost_pence, budget_pence, retrieved_count, started_at -> NUMBER
8
+ // completed_at -> STRING
9
+ function buildSignable(p) {
10
+ const hasExt = p.external_cost_hash !== undefined && p.external_cost_hash !== null;
11
+ const base = {
12
+ task_id: p.task_id,
13
+ agent_id: p.agent_id,
14
+ input_hash: p.input_hash,
15
+ output_hash: p.output_hash,
16
+ cost_pence: Number(p.cost_pence),
17
+ budget_pence: Number(p.budget_pence),
18
+ started_at: Number(p.started_at),
19
+ completed_at: String(p.completed_at),
20
+ };
21
+ if (hasExt) {
22
+ base.external_cost_hash = String(p.external_cost_hash);
23
+ base.retrieved_count = Number(p.retrieved_count ?? 0);
24
+ return { signable: base, fields: 10 };
25
+ }
26
+ return { signable: base, fields: 8 };
27
+ }
28
+ async function fetchJson(url) {
29
+ const res = await fetch(url);
30
+ if (!res.ok)
31
+ throw new Error(`fetch ${url} -> HTTP ${res.status}`);
32
+ return res.json();
33
+ }
34
+ // Trustless verification: fetch public key (+ optionally proof), reconstruct
35
+ // signable, verify Ed25519 locally. Server never asked "is this valid?".
36
+ export async function verifyProof(args) {
37
+ let proof = args.proof;
38
+ if (!proof) {
39
+ if (!args.task_id)
40
+ throw new Error('Provide task_id or proof');
41
+ const data = await fetchJson(`${FD_API}/v1/workforce/proof/${encodeURIComponent(args.task_id)}/public`);
42
+ if (!data?.proof)
43
+ throw new Error('proof_not_found');
44
+ proof = data.proof;
45
+ }
46
+ const keyData = await fetchJson(`${FD_API}/v1/workforce/proof/public-key`);
47
+ const pub = createPublicKey(keyData.public_key_pem);
48
+ const { signable, fields } = buildSignable(proof);
49
+ const digest = sha256hex(wfCanonical(signable));
50
+ let verified = false;
51
+ if (proof.signature && (proof.algorithm === 'Ed25519' || !proof.algorithm)) {
52
+ try {
53
+ verified = edVerify(null, Buffer.from(digest, 'hex'), pub, Buffer.from(String(proof.signature), 'base64'));
54
+ }
55
+ catch {
56
+ verified = false;
57
+ }
58
+ }
59
+ return {
60
+ verified,
61
+ task_id: proof.task_id,
62
+ key_id: keyData.key_id,
63
+ algorithm: 'Ed25519',
64
+ fields_signed: fields,
65
+ trustless: true,
66
+ message: verified
67
+ ? 'Signature mathematically verified. This proof was signed by ForceDream and has not been altered.'
68
+ : 'Signature verification FAILED. The proof was altered or not signed by ForceDream.',
69
+ note: 'Verified client-side via public-key cryptography. ForceDream was not asked whether the proof is valid.',
70
+ };
71
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@forcedream/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for ForceDream \u2014 discover, invoke, and trustlessly verify AI agents with cryptographic proofs.",
5
+ "type": "module",
6
+ "bin": {
7
+ "fd-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "verify-test": "node dist/selftest.js"
14
+ },
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.29.0",
20
+ "@noble/ed25519": "^2.1.0",
21
+ "zod": "^3.23.8"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "typescript": "^5.6.0"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "keywords": [
37
+ "mcp",
38
+ "model-context-protocol",
39
+ "forcedream",
40
+ "ai-agents",
41
+ "cryptographic-proof",
42
+ "ed25519",
43
+ "claude"
44
+ ],
45
+ "homepage": "https://www.forcedream.com",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/forcedreamai/forcedream.git",
49
+ "directory": "packages/mcp-server"
50
+ },
51
+ "author": "ForceDream"
52
+ }