@forcedream/mcp-server 0.5.1 → 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.
@@ -0,0 +1,68 @@
1
+ # Running the ForceDream MCP Server via Docker
2
+
3
+ **This is a stdio-based MCP server, not an HTTP service.** Confirmed directly against the
4
+ real source (`src/index.ts` uses `StdioServerTransport`, has no `process.env` reads, and
5
+ never calls `.listen()` on any port). It communicates over the container's stdin/stdout --
6
+ the same way a locally-run `npx @forcedream/mcp-server` process would. There is nothing to
7
+ `curl` and no port to publish; running it detached (`docker run -d`) will not work, because
8
+ nothing would ever be connected to its stdin.
9
+
10
+ ## Pull
11
+
12
+ ```bash
13
+ docker pull forcedream/forcedream-mcp
14
+ ```
15
+
16
+ ## Use with Claude Desktop (or any stdio-based MCP host)
17
+
18
+ Add this to your MCP client's config (for Claude Desktop,
19
+ `claude_desktop_config.json`):
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "forcedream": {
25
+ "command": "docker",
26
+ "args": ["run", "-i", "--rm", "forcedream/forcedream-mcp"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ - `-i` keeps stdin open -- required, since that's the entire transport.
33
+ - `--rm` cleans up the container on exit, since the client spawns a fresh one each session.
34
+
35
+ ## Run it directly, for manual testing
36
+
37
+ ```bash
38
+ docker run -i --rm forcedream/forcedream-mcp
39
+ ```
40
+
41
+ Then type a real MCP JSON-RPC request on stdin, e.g.:
42
+
43
+ ```json
44
+ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-test","version":"0.1"}}}
45
+ ```
46
+
47
+ and press Enter -- you should get a real `initialize` response back on stdout.
48
+
49
+ ## Passing environment variables (if/when needed)
50
+
51
+ The current server doesn't read any environment variables directly (confirmed against the
52
+ real source) -- individual tool calls (like `invoke_agent`) take a real API key as an MCP
53
+ tool argument, not a container-level secret. If that changes in a future version, pass
54
+ variables the standard way:
55
+
56
+ ```bash
57
+ docker run -i --rm -e SOME_VAR=value forcedream/forcedream-mcp
58
+ ```
59
+
60
+ ## Building locally
61
+
62
+ ```bash
63
+ docker build -t forcedream/forcedream-mcp .
64
+ ```
65
+
66
+ Multi-stage: the build stage installs `devDependencies` (needed for `tsc`), the runtime
67
+ stage installs only production dependencies and copies in the compiled `dist/` -- keeps the
68
+ final image smaller and free of build-only tooling.
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@forcedream/mcp-server.svg)](https://www.npmjs.com/package/@forcedream/mcp-server)
4
4
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
5
  [![Node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
6
+ [![smithery badge](https://smithery.ai/badge/forcedreamai/mcp-server)](https://smithery.ai/servers/forcedreamai/mcp-server)
6
7
 
7
8
  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.
8
9
 
@@ -15,23 +16,23 @@ Listed on the [official MCP Registry](https://registry.modelcontextprotocol.io)
15
16
  | **Transport** | stdio, runs on your machine | Streamable HTTP, hosted by ForceDream |
16
17
  | **Setup** | `npx -y @forcedream/mcp-server` | Point your client at `https://api.forcedream.ai/v1/mcp` |
17
18
  | **Auth for invoking** | `FD_API_KEY` env var | OAuth 2.1 + PKCE (standard MCP auth flow) |
18
- | **Tools available** | `search_agents`, `verify_proof`, `invoke_agent` | All of the above, plus `check_fraud`, `generate_embedding`, `market_quote` |
19
+ | **Tools available** | `forcedream_search_agents`, `forcedream_verify_proof`, `forcedream_invoke_agent` | All of the above, plus `forcedream_check_fraud`, `forcedream_generate_embedding`, `forcedream_market_quote` |
19
20
  | **Best for** | Claude Desktop, local dev | Any client with native remote-MCP + OAuth support |
20
21
 
21
22
  Both talk to the same real ForceDream API and the same real settlement system. Pick whichever fits your client.
22
23
 
23
24
  ## What it does
24
25
 
25
- `search_agents` and `verify_proof` need no account. Tools that spend your balance need authentication.
26
+ `forcedream_search_agents` and `forcedream_verify_proof` need no account. Tools that spend your balance need authentication.
26
27
 
27
28
  | Tool | Auth | What it does |
28
29
  |------|------|--------------|
29
- | `search_agents` | none | Discover ForceDream agents, their real capabilities, and honest, system-derived metrics. |
30
- | `verify_proof` | none | Independently verify a ForceDream proof by task ID. Checked locally against the published public key. |
31
- | `invoke_agent` | key/OAuth | Invoke an agent to do real work. Spends your balance. Honest declines and failed charges cost nothing. |
32
- | `check_fraud`\* | OAuth | Real-time fraud risk scoring using IP reputation and behavioural signals. |
33
- | `generate_embedding`\* | OAuth | Real 1024-dim text embeddings via Voyage voyage-3.5. |
34
- | `market_quote`\* | OAuth | Live stock quotes via Alpha Vantage, cached, WORM-sealed. |
30
+ | `forcedream_search_agents` | none | Discover ForceDream agents, their real capabilities, and honest, system-derived metrics. |
31
+ | `forcedream_verify_proof` | none | Independently verify a ForceDream proof by task ID. Checked locally against the published public key. |
32
+ | `forcedream_invoke_agent` | key/OAuth | Invoke an agent to do real work. Spends your balance. Honest declines and failed charges cost nothing. |
33
+ | `forcedream_check_fraud`\* | OAuth | Real-time fraud risk scoring using IP reputation and behavioural signals. |
34
+ | `forcedream_generate_embedding`\* | OAuth | Real 1024-dim text embeddings via Voyage voyage-3.5. |
35
+ | `forcedream_market_quote`\* | OAuth | Live stock quotes via Alpha Vantage, cached, WORM-sealed. |
35
36
 
36
37
  \* remote server only.
37
38
 
@@ -64,7 +65,7 @@ Edit your `claude_desktop_config.json`:
64
65
 
65
66
  Restart Claude Desktop. You should see the ForceDream tools available.
66
67
 
67
- > Omit `FD_API_KEY` to run discovery + verification only (no spending). Add it to enable `invoke_agent`.
68
+ > Omit `FD_API_KEY` to run discovery + verification only (no spending). Add it to enable `forcedream_invoke_agent`.
68
69
 
69
70
  ### 2b. Add to Cursor
70
71
 
@@ -112,7 +113,7 @@ Your client will handle the OAuth 2.1 + PKCE flow automatically the first time y
112
113
 
113
114
  ## Examples
114
115
 
115
- Real agents you can try, see search_agents for the full current list.
116
+ Real agents you can try, see forcedream_search_agents for the full current list.
116
117
 
117
118
  ```
118
119
  Invoke data-extract-v1 to pull structured fields from raw text.
@@ -178,7 +179,7 @@ Real prompts you can adapt, covering different real ways to use the tools togeth
178
179
  > Feed this sales history to forecast-generation-v1 and ask for a 3-month forecast.
179
180
 
180
181
  **Fraud check before a sensitive action** (remote only)
181
- > Before processing this withdrawal, run check_fraud on this user ID and IP address.
182
+ > Before processing this withdrawal, run forcedream_check_fraud on this user ID and IP address.
182
183
 
183
184
  **Market-aware research** (remote only)
184
185
  > Get a live quote for AAPL, then summarize what today's price move might mean for a tech-sector report.
@@ -253,9 +254,9 @@ None of these ever result in a double charge. A failed or pending task is never
253
254
 
254
255
  | Env var | Required | Default | Purpose |
255
256
  |---------|----------|---------|---------|
256
- | FD_API_KEY | only for invoke_agent | none | Your fd_live_ billing key. Spending happens against its balance. |
257
+ | FD_API_KEY | only for forcedream_invoke_agent | none | Your fd_live_ billing key. Spending happens against its balance. |
257
258
  | FD_API_BASE | no | https://api.forcedream.ai | Override the API base (for testing). |
258
- | FD_MOCK_MODE | no | unset | Set to "true" to test invoke_agent with synthetic, clearly-labeled fake results -- no real network call, no real balance spent. Never affects search_agents or verify_proof. |
259
+ | FD_MOCK_MODE | no | unset | Set to "true" to test forcedream_invoke_agent with synthetic, clearly-labeled fake results -- no real network call, no real balance spent. Never affects forcedream_search_agents or forcedream_verify_proof. |
259
260
 
260
261
  ## Run it directly
261
262
 
package/dist/index.js CHANGED
@@ -5,10 +5,19 @@ import { z } from 'zod';
5
5
  import { verifyProof } from './verify_proof.js';
6
6
  import { searchAgents, searchAgentsSchema } from './search_agents.js';
7
7
  import { invokeAgent, invokeAgentSchema } from './invoke_agent.js';
8
+ import { securityScan, securityScanSchema } from './security_scan.js';
8
9
  import { searchReliability, searchReliabilitySchema } from './search_reliability.js';
9
10
  import { searchCosts, searchCostsSchema } from './search_costs.js';
10
11
  import { searchProviders } from './search_providers.js';
11
- const server = new McpServer({ name: 'forcedream', version: '0.3.0' });
12
+ import { readFileSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { dirname, join } from 'node:path';
15
+ // Real fix for the version mismatch (this file previously hardcoded '0.3.0' as a separate,
16
+ // independently-drifting string, while package.json said '0.5.1'): read the one, real,
17
+ // canonical version at startup, so there is only ever one place this number lives.
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
20
+ const server = new McpServer({ name: 'forcedream', version: pkg.version });
12
21
  // forcedream_verify_proof — trustless, keyless. Verify a ForceDream proof's Ed25519 signature client-side.
13
22
  server.registerTool('forcedream_verify_proof', {
14
23
  title: 'Verify a ForceDream proof',
@@ -60,6 +69,25 @@ server.registerTool('forcedream_invoke_agent', {
60
69
  return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
61
70
  }
62
71
  });
72
+ // forcedream_security_scan — dedicated, named tool for security-scan-v1 specifically.
73
+ // Spends balance (needs FD_API_KEY). Same real invoke/poll pattern as forcedream_invoke_agent,
74
+ // fixed to this one agent so a caller doesn't need to know the generic agent_slug pattern.
75
+ server.registerTool('forcedream_security_scan', {
76
+ title: 'Scan code for security vulnerabilities',
77
+ description: 'Real security review for code: OWASP Top 10, injection, secrets, and dependency risks. ' +
78
+ 'Cross-references imported dependencies against OSV.dev for real CVEs, and scans for hardcoded ' +
79
+ 'secrets via GitGuardian\'s real-time detection (400+ types). SPENDS your balance — requires FD_API_KEY. ' +
80
+ 'Returns severity-graded findings, a risk score, what you were charged, and a proof_id.',
81
+ inputSchema: securityScanSchema,
82
+ }, async ({ code, max_wait_seconds }) => {
83
+ try {
84
+ const result = await securityScan({ code, max_wait_seconds });
85
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
86
+ }
87
+ catch (e) {
88
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
89
+ }
90
+ });
63
91
  // forcedream_search_reliability — keyless. Real, system-measured reliability per agent.
64
92
  server.registerTool('forcedream_search_reliability', {
65
93
  title: 'Search agent reliability data',
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod';
2
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
3
+ const SLUG = 'security-scan-v1';
4
+ /**
5
+ * Zod input schema for the forcedream_security_scan tool. code is required;
6
+ * max_wait_seconds bounds how long this call polls before returning a pollable task_id.
7
+ * A dedicated, named tool for ForceDream's security-scan-v1 agent, rather than requiring
8
+ * a caller to know the generic forcedream_invoke_agent + agent_slug pattern.
9
+ */
10
+ export const securityScanSchema = {
11
+ code: z.string().describe('The code to scan for security vulnerabilities (OWASP Top 10, injection, secrets, dependency risks).'),
12
+ max_wait_seconds: z.number().optional().describe('Max seconds to poll (default 60, agent typically takes ~45s). On timeout, returns task_id to poll later.'),
13
+ };
14
+ function authHeader() {
15
+ const key = process.env.FD_API_KEY || '';
16
+ return key ? { Authorization: `Bearer ${key}` } : {};
17
+ }
18
+ async function postJson(url, body) {
19
+ const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader() }, body: JSON.stringify(body) });
20
+ let json = null;
21
+ try {
22
+ json = await res.json();
23
+ }
24
+ catch { }
25
+ return { status: res.status, json };
26
+ }
27
+ async function getJson(url) {
28
+ const res = await fetch(url, { headers: authHeader() });
29
+ let json = null;
30
+ try {
31
+ json = await res.json();
32
+ }
33
+ catch { }
34
+ return { status: res.status, json };
35
+ }
36
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
37
+ // Invoke security-scan-v1 and wait (bounded) for the result. SPENDS balance -- needs FD_API_KEY.
38
+ // Invokes ONCE; on timeout does NOT re-invoke (would double-charge), returns task_id instead.
39
+ // Same real invoke/poll pattern as forcedream_invoke_agent, fixed to this one, specific agent.
40
+ /**
41
+ * Invokes ForceDream's real security-scan-v1 agent and polls (bounded) for the result.
42
+ * SPENDS your balance -- requires FD_API_KEY. Invokes once; never re-invokes on timeout
43
+ * (would double-charge) -- returns a pollable task_id instead. Set FD_MOCK_MODE=true to
44
+ * test without spending real balance.
45
+ * @param args.code - The code to scan.
46
+ * @param args.max_wait_seconds - Max seconds to poll before returning a pollable task_id (default 60, max 120).
47
+ */
48
+ export async function securityScan(args) {
49
+ // Mock mode: explicit opt-in only, never a default. Intercepts BEFORE any real network
50
+ // call -- no real balance touched, no real agent invoked. Same discipline as invoke_agent.ts.
51
+ if (process.env.FD_MOCK_MODE === 'true') {
52
+ return {
53
+ status: 'completed',
54
+ agent: SLUG,
55
+ task_id: 'mock_' + Date.now(),
56
+ output: { mock: true, findings: [], risk_score: 0, note: 'Synthetic mock output. This is not a real scan result.' },
57
+ charged_pence: 0,
58
+ mock: true,
59
+ message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real scan was run, no balance was spent, and this response has no real proof_id -- forcedream_verify_proof will correctly reject it if you try. Unset FD_MOCK_MODE to run real scans.',
60
+ };
61
+ }
62
+ if (!process.env.FD_API_KEY) {
63
+ return { status: 'error', agent: SLUG, message: 'FD_API_KEY is required to scan (scanning spends your balance). Set it in the MCP server env. forcedream_search_agents and forcedream_verify_proof need no key.' };
64
+ }
65
+ const maxWaitMs = Math.max(5, Math.min(120, args.max_wait_seconds ?? 60)) * 1000;
66
+ const inv = await postJson(`${FD_API}/v1/agents/${SLUG}/invoke`, { task: args.code });
67
+ if (inv.status === 401)
68
+ return { status: 'error', agent: SLUG, message: 'Invalid FD_API_KEY (401). Check the key in the MCP server env.' };
69
+ if (!inv.json?.task_id)
70
+ return { status: 'error', agent: SLUG, message: `Invoke failed (HTTP ${inv.status}): ${inv.json?.error || inv.json?.note || 'no task_id'}` };
71
+ const taskId = inv.json.task_id;
72
+ const start = Date.now();
73
+ let intervalMs = 2500;
74
+ while (Date.now() - start < maxWaitMs) {
75
+ await sleep(intervalMs);
76
+ const poll = await getJson(`${FD_API}/v1/agents/${SLUG}/result/${encodeURIComponent(taskId)}`);
77
+ const d = poll.json || {};
78
+ const status = d.status || d.outcome;
79
+ if (status === 'completed' || status === 'succeeded' || d.ok === true) {
80
+ return {
81
+ status: 'completed', agent: SLUG, task_id: taskId, output: d.output, charged_pence: d.charged_pence,
82
+ proof_id: d.proof_id || taskId,
83
+ verify_proof_hint: `Verify trustlessly: call forcedream_verify_proof with task_id "${d.proof_id || taskId}". The signature proves authenticity without trusting ForceDream.`,
84
+ message: `Completed. Charged ${d.charged_pence}p. Cryptographically proven (proof_id ${d.proof_id || taskId}).`,
85
+ };
86
+ }
87
+ if (status === 'charge_failed')
88
+ 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.` };
89
+ if (status === 'failed' || status === 'dead_letter')
90
+ return { status: 'error', agent: SLUG, task_id: taskId, message: `Task ${status}: ${d.reason || d.last_error || 'unknown'}` };
91
+ intervalMs = Math.min(intervalMs + 1000, 6000);
92
+ }
93
+ 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.` };
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcedream/mcp-server",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "mcpName": "io.github.forcedreamai/mcp-server",
5
5
  "description": "MCP server for ForceDream \u2014 discover, invoke, and trustlessly verify AI agents with cryptographic proofs.",
6
6
  "type": "module",