@forcedream/mcp-server 0.5.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.docker.md +68 -0
- package/README.md +14 -13
- package/dist/index.js +35 -7
- package/dist/invoke_agent.js +8 -7
- package/dist/search_agents.js +1 -1
- package/dist/search_costs.js +1 -1
- package/dist/search_reliability.js +1 -1
- package/dist/security_scan.js +94 -0
- package/package.json +1 -1
package/README.docker.md
ADDED
|
@@ -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
|
[](https://www.npmjs.com/package/@forcedream/mcp-server)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
|
+
[](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** | `
|
|
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
|
-
`
|
|
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
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,11 +5,20 @@ 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
|
-
|
|
12
|
-
|
|
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 });
|
|
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',
|
|
15
24
|
description: 'Independently verify that a ForceDream agent proof is authentic and untampered, using public-key cryptography. ' +
|
|
@@ -28,7 +37,7 @@ server.registerTool('forcedream_verify_proof', {
|
|
|
28
37
|
return { content: [{ type: 'text', text: JSON.stringify({ verified: false, error: e.message }, null, 2) }], isError: true };
|
|
29
38
|
}
|
|
30
39
|
});
|
|
31
|
-
//
|
|
40
|
+
// forcedream_search_agents — keyless discovery of real agents with honest, system-derived metrics.
|
|
32
41
|
server.registerTool('forcedream_search_agents', {
|
|
33
42
|
title: 'Search ForceDream agents',
|
|
34
43
|
description: 'Discover ForceDream agents and their honest, system-derived metrics (proof_count, success_rate). ' +
|
|
@@ -44,7 +53,7 @@ server.registerTool('forcedream_search_agents', {
|
|
|
44
53
|
return { content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
45
54
|
}
|
|
46
55
|
});
|
|
47
|
-
//
|
|
56
|
+
// forcedream_invoke_agent — spends balance (needs FD_API_KEY). Returns output + a verifiable proof_id.
|
|
48
57
|
server.registerTool('forcedream_invoke_agent', {
|
|
49
58
|
title: 'Invoke a ForceDream agent',
|
|
50
59
|
description: 'Invoke a ForceDream agent to do real work. SPENDS your balance — requires FD_API_KEY in the server env. ' +
|
|
@@ -60,7 +69,26 @@ 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
|
});
|
|
63
|
-
//
|
|
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
|
+
});
|
|
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',
|
|
66
94
|
description: 'Real, system-measured reliability per agent: success_rate, avg_latency_ms, sample_size. No key needed. ' +
|
|
@@ -75,7 +103,7 @@ server.registerTool('forcedream_search_reliability', {
|
|
|
75
103
|
return { content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
76
104
|
}
|
|
77
105
|
});
|
|
78
|
-
//
|
|
106
|
+
// forcedream_search_costs — keyless. Real price_per_call_pence per agent.
|
|
79
107
|
server.registerTool('forcedream_search_costs', {
|
|
80
108
|
title: 'Search agent pricing',
|
|
81
109
|
description: 'Real price_per_call_pence for every registered agent. No key needed. ' +
|
|
@@ -90,7 +118,7 @@ server.registerTool('forcedream_search_costs', {
|
|
|
90
118
|
return { content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
91
119
|
}
|
|
92
120
|
});
|
|
93
|
-
//
|
|
121
|
+
// forcedream_search_providers — keyless. Real, live inference-provider health.
|
|
94
122
|
server.registerTool('forcedream_search_providers', {
|
|
95
123
|
title: 'Search provider health',
|
|
96
124
|
description: 'Real, live inference-provider health: health_score, breaker_state, uptime_ratio, recent successes/failures. ' +
|
package/dist/invoke_agent.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
|
|
3
3
|
/**
|
|
4
|
-
* Zod input schema for the
|
|
4
|
+
* Zod input schema for the forcedream_invoke_agent tool. agent_slug and task are required;
|
|
5
5
|
* max_wait_seconds bounds how long this call polls before returning a pollable task_id.
|
|
6
6
|
*/
|
|
7
7
|
export const invokeAgentSchema = {
|
|
8
|
-
agent_slug: z.string().describe('The agent to invoke, e.g. "atlas-research-v1". Use
|
|
8
|
+
agent_slug: z.string().describe('The agent to invoke, e.g. "atlas-research-v1". Use forcedream_search_agents to discover.'),
|
|
9
9
|
task: z.string().describe('The task/query for the agent (Atlas: a research question).'),
|
|
10
10
|
max_wait_seconds: z.number().optional().describe('Max seconds to poll (default 60). On timeout, returns task_id to poll later.'),
|
|
11
11
|
};
|
|
@@ -45,8 +45,9 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
45
45
|
export async function invokeAgent(args) {
|
|
46
46
|
// Mock mode: explicit opt-in only, never a default. Intercepts BEFORE any real network call --
|
|
47
47
|
// no real balance touched, no real agent invoked. Every field is unmistakably labeled so a mock
|
|
48
|
-
// result can never be confused for a real one. Scoped to
|
|
49
|
-
//
|
|
48
|
+
// result can never be confused for a real one. Scoped to forcedream_invoke_agent only:
|
|
49
|
+
// forcedream_search_agents and forcedream_verify_proof are already free and keyless, so mocking
|
|
50
|
+
// them would only make testing worse.
|
|
50
51
|
if (process.env.FD_MOCK_MODE === 'true') {
|
|
51
52
|
return {
|
|
52
53
|
status: 'completed',
|
|
@@ -55,11 +56,11 @@ export async function invokeAgent(args) {
|
|
|
55
56
|
output: { mock: true, note: 'Synthetic mock output. This is not real ForceDream data.' },
|
|
56
57
|
charged_pence: 0,
|
|
57
58
|
mock: true,
|
|
58
|
-
message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real agent was invoked, no balance was spent, and this response has no real proof_id --
|
|
59
|
+
message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real agent was invoked, 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 invoke real agents.',
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
if (!process.env.FD_API_KEY) {
|
|
62
|
-
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.
|
|
63
|
+
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. forcedream_search_agents and forcedream_verify_proof need no key.' };
|
|
63
64
|
}
|
|
64
65
|
const slug = args.agent_slug;
|
|
65
66
|
const maxWaitMs = Math.max(5, Math.min(120, args.max_wait_seconds ?? 60)) * 1000;
|
|
@@ -83,7 +84,7 @@ export async function invokeAgent(args) {
|
|
|
83
84
|
return {
|
|
84
85
|
status: 'completed', agent: slug, task_id: taskId, output: d.output, charged_pence: d.charged_pence,
|
|
85
86
|
proof_id: d.proof_id || taskId,
|
|
86
|
-
verify_proof_hint: `Verify trustlessly: call
|
|
87
|
+
verify_proof_hint: `Verify trustlessly: call forcedream_verify_proof with task_id "${d.proof_id || taskId}". The signature proves authenticity without trusting ForceDream.`,
|
|
87
88
|
message: `Completed. Charged ${d.charged_pence}p. Cryptographically proven (proof_id ${d.proof_id || taskId}).`,
|
|
88
89
|
};
|
|
89
90
|
}
|
package/dist/search_agents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
|
|
3
3
|
/**
|
|
4
|
-
* Zod input schema for the
|
|
4
|
+
* Zod input schema for the forcedream_search_agents tool. Both fields optional -- omitting both
|
|
5
5
|
* returns the full real agent registry, merged with live reliability data.
|
|
6
6
|
*/
|
|
7
7
|
export const searchAgentsSchema = {
|
package/dist/search_costs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
|
|
3
3
|
/**
|
|
4
|
-
* Zod input schema for
|
|
4
|
+
* Zod input schema for forcedream_search_costs. Optional max_price_pence filter for budget-aware selection.
|
|
5
5
|
*/
|
|
6
6
|
export const searchCostsSchema = {
|
|
7
7
|
max_price_pence: z.number().optional().describe('Optional: only return agents at or under this price.'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
|
|
3
3
|
/**
|
|
4
|
-
* Zod input schema for
|
|
4
|
+
* Zod input schema for forcedream_search_reliability. Optional agent_slug filter; omit for all agents.
|
|
5
5
|
*/
|
|
6
6
|
export const searchReliabilitySchema = {
|
|
7
7
|
agent_slug: z.string().optional().describe('Optional: filter to one agent slug. Omit to return all.'),
|
|
@@ -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.
|
|
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",
|