@forcedream/mcp-server 0.6.2 → 0.8.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,91 @@
1
+ import { z } from 'zod';
2
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
3
+ function authHeader() {
4
+ const key = process.env.FD_API_KEY || '';
5
+ return key ? { Authorization: `Bearer ${key}` } : {};
6
+ }
7
+ async function postJson(url, body) {
8
+ const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader() }, body: JSON.stringify(body) });
9
+ let json = null;
10
+ try {
11
+ json = await res.json();
12
+ }
13
+ catch { }
14
+ return { status: res.status, json };
15
+ }
16
+ // --- forcedream_check_fraud ---
17
+ export const checkFraudSchema = {
18
+ ip: z.string().optional().describe('Optional IP address to check against AbuseIPDB reputation data.'),
19
+ };
20
+ /**
21
+ * Real, direct fraud risk assessment -- a single, synchronous call, no polling. SPENDS
22
+ * your balance -- requires FD_API_KEY. Uses real AbuseIPDB reputation data when an IP is
23
+ * provided.
24
+ * @param args.ip - Optional IP address to check.
25
+ */
26
+ export async function checkFraud(args) {
27
+ if (process.env.FD_MOCK_MODE === 'true') {
28
+ return { status: 'completed', risk_score: 0, signals: {}, verdict: 'allow', ip_reputation: { abuseipdb_score: null }, mock: true, message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real balance was spent, no real AbuseIPDB lookup was made.' };
29
+ }
30
+ if (!process.env.FD_API_KEY) {
31
+ return { status: 'error', message: 'FD_API_KEY is required (this tool spends your balance). Set it in the MCP server env.' };
32
+ }
33
+ const res = await postJson(`${FD_API}/v1/tools/check-fraud`, { ip: args.ip || '' });
34
+ if (res.status === 401)
35
+ return { status: 'error', message: 'Invalid FD_API_KEY (401).' };
36
+ if (res.status === 402)
37
+ return { status: 'error', message: `Insufficient balance: ${JSON.stringify(res.json)}` };
38
+ if (res.status !== 200 || !res.json)
39
+ return { status: 'error', message: `Request failed (HTTP ${res.status}): ${JSON.stringify(res.json)}` };
40
+ return { status: 'completed', ...res.json };
41
+ }
42
+ // --- forcedream_generate_embedding ---
43
+ export const generateEmbeddingSchema = {
44
+ text: z.string().describe('Text to embed (max ~32000 chars).'),
45
+ input_type: z.string().optional().describe('Optional: "query" or "document".'),
46
+ };
47
+ /**
48
+ * Real, direct text embedding via Voyage voyage-3.5 -- a single, synchronous call, no
49
+ * polling. SPENDS your balance (per-token charge) -- requires FD_API_KEY.
50
+ * @param args.text - Text to embed.
51
+ * @param args.input_type - Optional "query" or "document".
52
+ */
53
+ export async function generateEmbedding(args) {
54
+ if (process.env.FD_MOCK_MODE === 'true') {
55
+ return { status: 'completed', dimensions: 1024, tokens: 0, embedding: [], cost_pence: 0, mock: true, message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real balance was spent, no real embedding was generated.' };
56
+ }
57
+ if (!process.env.FD_API_KEY) {
58
+ return { status: 'error', message: 'FD_API_KEY is required (this tool spends your balance). Set it in the MCP server env.' };
59
+ }
60
+ const res = await postJson(`${FD_API}/v1/embeddings`, { text: args.text, input_type: args.input_type });
61
+ if (res.status === 401)
62
+ return { status: 'error', message: 'Invalid FD_API_KEY (401).' };
63
+ if (res.status !== 200 || !res.json)
64
+ return { status: 'error', message: `Request failed (HTTP ${res.status}): ${JSON.stringify(res.json)}` };
65
+ return { status: 'completed', ...res.json };
66
+ }
67
+ // --- forcedream_market_quote ---
68
+ export const marketQuoteSchema = {
69
+ symbol: z.string().describe('Ticker symbol, e.g. "AAPL", "IBM".'),
70
+ };
71
+ /**
72
+ * Real, direct, live market quote via Alpha Vantage -- a single, synchronous call, no
73
+ * polling. SPENDS your balance -- requires FD_API_KEY. Hard-cached server-side.
74
+ * @param args.symbol - Ticker symbol, e.g. "AAPL".
75
+ */
76
+ export async function marketQuote(args) {
77
+ if (process.env.FD_MOCK_MODE === 'true') {
78
+ return { status: 'completed', symbol: args.symbol, price: 0, change_percent: 0, volume: 0, mock: true, message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real balance was spent, no real market data was fetched.' };
79
+ }
80
+ if (!process.env.FD_API_KEY) {
81
+ return { status: 'error', message: 'FD_API_KEY is required (this tool spends your balance). Set it in the MCP server env.' };
82
+ }
83
+ const res = await postJson(`${FD_API}/v1/tools/market-quote`, { symbol: args.symbol });
84
+ if (res.status === 401)
85
+ return { status: 'error', message: 'Invalid FD_API_KEY (401).' };
86
+ if (res.status === 402)
87
+ return { status: 'error', message: `Insufficient balance: ${JSON.stringify(res.json)}` };
88
+ if (res.status !== 200 || !res.json)
89
+ return { status: 'error', message: `Request failed (HTTP ${res.status}): ${JSON.stringify(res.json)}` };
90
+ return { status: 'completed', ...res.json };
91
+ }
@@ -0,0 +1,100 @@
1
+ import { z } from 'zod';
2
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
3
+ const SLUG = 'data-extract-v1';
4
+ /**
5
+ * Zod input schema for the forcedream_extract_data tool. fields and document are 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 data-extract-v1 agent, rather than requiring
8
+ * a caller to know the generic forcedream_invoke_agent + agent_slug pattern.
9
+ */
10
+ export const extractDataSchema = {
11
+ fields: z.array(z.string()).describe('The field names to extract, e.g. ["company_name", "ceo_name"].'),
12
+ document: z.string().describe('The unstructured document text to extract from.'),
13
+ max_wait_seconds: z.number().optional().describe('Max seconds to poll (default 60, agent typically takes ~30s). On timeout, returns task_id to poll later.'),
14
+ };
15
+ function authHeader() {
16
+ const key = process.env.FD_API_KEY || '';
17
+ return key ? { Authorization: `Bearer ${key}` } : {};
18
+ }
19
+ async function postJson(url, body) {
20
+ const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader() }, body: JSON.stringify(body) });
21
+ let json = null;
22
+ try {
23
+ json = await res.json();
24
+ }
25
+ catch { }
26
+ return { status: res.status, json };
27
+ }
28
+ async function getJson(url) {
29
+ const res = await fetch(url, { headers: authHeader() });
30
+ let json = null;
31
+ try {
32
+ json = await res.json();
33
+ }
34
+ catch { }
35
+ return { status: res.status, json };
36
+ }
37
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
38
+ // Invoke data-extract-v1 and wait (bounded) for the result. SPENDS balance -- needs FD_API_KEY.
39
+ // Invokes ONCE; on timeout does NOT re-invoke (would double-charge), returns task_id instead.
40
+ // Same real invoke/poll pattern as forcedream_invoke_agent / forcedream_security_scan, fixed
41
+ // to this one, specific agent. fields + document are combined into the agent's real,
42
+ // established "Fields to extract: ...\n\nDocument: ..." task format internally.
43
+ /**
44
+ * Invokes ForceDream's real data-extract-v1 agent and polls (bounded) for the result.
45
+ * SPENDS your balance -- requires FD_API_KEY. Invokes once; never re-invokes on timeout
46
+ * (would double-charge) -- returns a pollable task_id instead. Set FD_MOCK_MODE=true to
47
+ * test without spending real balance.
48
+ * @param args.fields - The field names to extract.
49
+ * @param args.document - The document text to extract from.
50
+ * @param args.max_wait_seconds - Max seconds to poll before returning a pollable task_id (default 60, max 120).
51
+ */
52
+ export async function extractData(args) {
53
+ // Mock mode: explicit opt-in only, never a default. Intercepts BEFORE any real network
54
+ // call -- no real balance touched, no real agent invoked. Same discipline as invoke_agent.ts
55
+ // and security_scan.ts.
56
+ if (process.env.FD_MOCK_MODE === 'true') {
57
+ return {
58
+ status: 'completed',
59
+ agent: SLUG,
60
+ task_id: 'mock_' + Date.now(),
61
+ output: { rows: [], extracted_fields: [], missing_fields: args.fields, entity_verification: [], note: 'Synthetic mock output. This is not a real extraction result.' },
62
+ charged_pence: 0,
63
+ mock: true,
64
+ message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real extraction 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 extractions.',
65
+ };
66
+ }
67
+ if (!process.env.FD_API_KEY) {
68
+ return { status: 'error', agent: SLUG, message: 'FD_API_KEY is required to extract (extracting spends your balance). Set it in the MCP server env. forcedream_search_agents and forcedream_verify_proof need no key.' };
69
+ }
70
+ const maxWaitMs = Math.max(5, Math.min(120, args.max_wait_seconds ?? 60)) * 1000;
71
+ const task = `Fields to extract: ${args.fields.join(', ')}. Document: ${args.document}`;
72
+ const inv = await postJson(`${FD_API}/v1/agents/${SLUG}/invoke`, { task });
73
+ if (inv.status === 401)
74
+ return { status: 'error', agent: SLUG, message: 'Invalid FD_API_KEY (401). Check the key in the MCP server env.' };
75
+ if (!inv.json?.task_id)
76
+ return { status: 'error', agent: SLUG, message: `Invoke failed (HTTP ${inv.status}): ${inv.json?.error || inv.json?.note || 'no task_id'}` };
77
+ const taskId = inv.json.task_id;
78
+ const start = Date.now();
79
+ let intervalMs = 2500;
80
+ while (Date.now() - start < maxWaitMs) {
81
+ await sleep(intervalMs);
82
+ const poll = await getJson(`${FD_API}/v1/agents/${SLUG}/result/${encodeURIComponent(taskId)}`);
83
+ const d = poll.json || {};
84
+ const status = d.status || d.outcome;
85
+ if (status === 'completed' || status === 'succeeded' || d.ok === true) {
86
+ return {
87
+ status: 'completed', agent: SLUG, task_id: taskId, output: d.output, charged_pence: d.charged_pence,
88
+ proof_id: d.proof_id || taskId,
89
+ verify_proof_hint: `Verify trustlessly: call forcedream_verify_proof with task_id "${d.proof_id || taskId}". The signature proves authenticity without trusting ForceDream.`,
90
+ message: `Completed. Charged ${d.charged_pence}p. Cryptographically proven (proof_id ${d.proof_id || taskId}).`,
91
+ };
92
+ }
93
+ if (status === 'charge_failed')
94
+ 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.` };
95
+ if (status === 'failed' || status === 'dead_letter')
96
+ return { status: 'error', agent: SLUG, task_id: taskId, message: `Task ${status}: ${d.reason || d.last_error || 'unknown'}` };
97
+ intervalMs = Math.min(intervalMs + 1000, 6000);
98
+ }
99
+ 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.` };
100
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ 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
8
  import { securityScan, securityScanSchema } from './security_scan.js';
9
+ import { extractData, extractDataSchema } from './extract_data.js';
10
+ import { checkFraud, checkFraudSchema, generateEmbedding, generateEmbeddingSchema, marketQuote, marketQuoteSchema } from './direct_tools.js';
9
11
  import { searchReliability, searchReliabilitySchema } from './search_reliability.js';
10
12
  import { searchCosts, searchCostsSchema } from './search_costs.js';
11
13
  import { searchProviders } from './search_providers.js';
@@ -88,6 +90,74 @@ server.registerTool('forcedream_security_scan', {
88
90
  return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
89
91
  }
90
92
  });
93
+ // forcedream_extract_data — dedicated, named tool for data-extract-v1 specifically.
94
+ // Spends balance (needs FD_API_KEY). Same real invoke/poll pattern as forcedream_invoke_agent,
95
+ // fixed to this one agent so a caller doesn't need to know the generic agent_slug pattern.
96
+ server.registerTool('forcedream_extract_data', {
97
+ title: 'Extract structured fields from a document',
98
+ description: 'Structured JSON extraction from unstructured text -- grounded in real, live verification, not just ' +
99
+ 'pattern-matching. Pulls requested fields, nulls anything missing, never guesses. Cross-references detected ' +
100
+ 'proper-noun entities (companies, people, places) against Wikidata to confirm which extracted values are ' +
101
+ 'independently verified vs. unconfirmed. SPENDS your balance — requires FD_API_KEY. Returns the extracted ' +
102
+ 'rows, what you were charged, and a proof_id.',
103
+ inputSchema: extractDataSchema,
104
+ }, async ({ fields, document, max_wait_seconds }) => {
105
+ try {
106
+ const result = await extractData({ fields, document, max_wait_seconds });
107
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
108
+ }
109
+ catch (e) {
110
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
111
+ }
112
+ });
113
+ // forcedream_check_fraud — real, direct, synchronous call (no polling). Spends balance
114
+ // (needs FD_API_KEY). Uses the new, dedicated fd_live_-accepting backend endpoint.
115
+ server.registerTool('forcedream_check_fraud', {
116
+ title: 'Check IP / account fraud risk',
117
+ description: 'Real fraud risk assessment using AbuseIPDB IP reputation data. SPENDS your balance — requires FD_API_KEY. ' +
118
+ 'Returns risk_score, signals, and an allow/review/block verdict, WORM-sealed.',
119
+ inputSchema: checkFraudSchema,
120
+ }, async ({ ip }) => {
121
+ try {
122
+ const result = await checkFraud({ ip });
123
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
124
+ }
125
+ catch (e) {
126
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
127
+ }
128
+ });
129
+ // forcedream_generate_embedding — real, direct, synchronous call (no polling). Spends
130
+ // balance (needs FD_API_KEY, per-token charge).
131
+ server.registerTool('forcedream_generate_embedding', {
132
+ title: 'Generate a text embedding',
133
+ description: 'Real 1024-dim vector embedding via Voyage voyage-3.5, retrieval-optimised. SPENDS your balance ' +
134
+ '(per-token charge) — requires FD_API_KEY. Returns the vector, dimensions, token count, WORM-sealed.',
135
+ inputSchema: generateEmbeddingSchema,
136
+ }, async ({ text, input_type }) => {
137
+ try {
138
+ const result = await generateEmbedding({ text, input_type });
139
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
140
+ }
141
+ catch (e) {
142
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
143
+ }
144
+ });
145
+ // forcedream_market_quote — real, direct, synchronous call (no polling). Spends balance
146
+ // (needs FD_API_KEY). Hard-cached server-side.
147
+ server.registerTool('forcedream_market_quote', {
148
+ title: 'Get a live market quote',
149
+ description: 'Real, live market quote for a stock symbol via Alpha Vantage: price, change %, volume, day high/low, ' +
150
+ 'liquidity score. SPENDS your balance — requires FD_API_KEY. Hard-cached, WORM-sealed.',
151
+ inputSchema: marketQuoteSchema,
152
+ }, async ({ symbol }) => {
153
+ try {
154
+ const result = await marketQuote({ symbol });
155
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
156
+ }
157
+ catch (e) {
158
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
159
+ }
160
+ });
91
161
  // forcedream_search_reliability — keyless. Real, system-measured reliability per agent.
92
162
  server.registerTool('forcedream_search_reliability', {
93
163
  title: 'Search agent reliability data',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcedream/mcp-server",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "mcpName": "io.github.forcedreamai/mcp-server",
5
5
  "description": "Discover, invoke, and trustlessly verify ForceDream AI agents with cryptographic proofs. ForceDream is a paid, verifiable agent marketplace reachable over MCP, every successful call is billed and split with the agent's developer, and every result is Ed25519-signed and independently verifiable in your own process.",
6
6
  "type": "module",