@forcedream/mcp-server 0.7.0 → 0.9.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
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ 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
9
  import { extractData, extractDataSchema } from './extract_data.js';
10
+ import { checkFraud, checkFraudSchema, generateEmbedding, generateEmbeddingSchema, marketQuote, marketQuoteSchema } from './direct_tools.js';
11
+ import { scoreLead, scoreLeadSchema } from './score_lead.js';
10
12
  import { searchReliability, searchReliabilitySchema } from './search_reliability.js';
11
13
  import { searchCosts, searchCostsSchema } from './search_costs.js';
12
14
  import { searchProviders } from './search_providers.js';
@@ -109,6 +111,74 @@ server.registerTool('forcedream_extract_data', {
109
111
  return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
110
112
  }
111
113
  });
114
+ // forcedream_score_lead — dedicated, named tool for lead-score-v1 specifically.
115
+ // Spends balance (needs FD_API_KEY). Same real invoke/poll pattern as forcedream_extract_data,
116
+ // fixed to this one agent so a caller doesn't need to know the generic agent_slug pattern.
117
+ server.registerTool('forcedream_score_lead', {
118
+ title: 'Score a sales lead',
119
+ description: 'Score sales leads hot/warm/cold with weighted signals and a recommended next action — grounded in real, ' +
120
+ 'live verification. Cross-references detected companies, domains, and locations against 8 real sources: ' +
121
+ 'Wikidata, UK Companies House, EU VIES VAT validation, postcodes.io, Google PageSpeed Insights, ' +
122
+ 'OpenStreetMap Nominatim, DNS/MX records, and live HTTP checks. Global by design — 5 sources work for any ' +
123
+ 'lead worldwide; 3 regional ones (UK/EU) apply only when genuinely detected. SPENDS your balance — requires FD_API_KEY.',
124
+ inputSchema: scoreLeadSchema,
125
+ }, async ({ lead_description, max_wait_seconds }) => {
126
+ try {
127
+ const result = await scoreLead({ lead_description, max_wait_seconds });
128
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
129
+ }
130
+ catch (e) {
131
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
132
+ }
133
+ });
134
+ // forcedream_check_fraud — real, direct, synchronous call (no polling). Spends balance
135
+ // (needs FD_API_KEY). Uses the new, dedicated fd_live_-accepting backend endpoint.
136
+ server.registerTool('forcedream_check_fraud', {
137
+ title: 'Check IP / account fraud risk',
138
+ description: 'Real fraud risk assessment using AbuseIPDB IP reputation data. SPENDS your balance — requires FD_API_KEY. ' +
139
+ 'Returns risk_score, signals, and an allow/review/block verdict, WORM-sealed.',
140
+ inputSchema: checkFraudSchema,
141
+ }, async ({ ip }) => {
142
+ try {
143
+ const result = await checkFraud({ ip });
144
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
145
+ }
146
+ catch (e) {
147
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
148
+ }
149
+ });
150
+ // forcedream_generate_embedding — real, direct, synchronous call (no polling). Spends
151
+ // balance (needs FD_API_KEY, per-token charge).
152
+ server.registerTool('forcedream_generate_embedding', {
153
+ title: 'Generate a text embedding',
154
+ description: 'Real 1024-dim vector embedding via Voyage voyage-3.5, retrieval-optimised. SPENDS your balance ' +
155
+ '(per-token charge) — requires FD_API_KEY. Returns the vector, dimensions, token count, WORM-sealed.',
156
+ inputSchema: generateEmbeddingSchema,
157
+ }, async ({ text, input_type }) => {
158
+ try {
159
+ const result = await generateEmbedding({ text, input_type });
160
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
161
+ }
162
+ catch (e) {
163
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
164
+ }
165
+ });
166
+ // forcedream_market_quote — real, direct, synchronous call (no polling). Spends balance
167
+ // (needs FD_API_KEY). Hard-cached server-side.
168
+ server.registerTool('forcedream_market_quote', {
169
+ title: 'Get a live market quote',
170
+ description: 'Real, live market quote for a stock symbol via Alpha Vantage: price, change %, volume, day high/low, ' +
171
+ 'liquidity score. SPENDS your balance — requires FD_API_KEY. Hard-cached, WORM-sealed.',
172
+ inputSchema: marketQuoteSchema,
173
+ }, async ({ symbol }) => {
174
+ try {
175
+ const result = await marketQuote({ symbol });
176
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
177
+ }
178
+ catch (e) {
179
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
180
+ }
181
+ });
112
182
  // forcedream_search_reliability — keyless. Real, system-measured reliability per agent.
113
183
  server.registerTool('forcedream_search_reliability', {
114
184
  title: 'Search agent reliability data',
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+ const FD_API = process.env.FD_API_BASE || 'https://api.forcedream.ai';
3
+ const SLUG = 'lead-score-v1';
4
+ /**
5
+ * Zod input schema for the forcedream_score_lead tool. lead_description 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 lead-score-v1 agent, rather than requiring
8
+ * a caller to know the generic forcedream_invoke_agent + agent_slug pattern.
9
+ */
10
+ export const scoreLeadSchema = {
11
+ lead_description: z.string().describe('Free-text description of the lead: company, contact, context, any details you have.'),
12
+ max_wait_seconds: z.number().optional().describe('Max seconds to poll (default 60, agent typically takes up to 45s given multi-source enrichment). 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 lead-score-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 the other dedicated tools, fixed to this one agent.
40
+ /**
41
+ * Invokes ForceDream's real lead-score-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.lead_description - Free-text description of the lead.
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 scoreLead(args) {
49
+ if (process.env.FD_MOCK_MODE === 'true') {
50
+ return {
51
+ status: 'completed',
52
+ agent: SLUG,
53
+ task_id: 'mock_' + Date.now(),
54
+ output: { tier: 'warm', score: 50, signals: [], recommended_action: 'mock', summary: 'mock', note: 'Synthetic mock output. This is not a real lead score.' },
55
+ charged_pence: 0,
56
+ mock: true,
57
+ message: 'MOCK MODE ACTIVE (FD_MOCK_MODE=true): no real scoring 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 scoring.',
58
+ };
59
+ }
60
+ if (!process.env.FD_API_KEY) {
61
+ return { status: 'error', agent: SLUG, message: 'FD_API_KEY is required to score (scoring spends your balance). Set it in the MCP server env. forcedream_search_agents and forcedream_verify_proof need no key.' };
62
+ }
63
+ const maxWaitMs = Math.max(5, Math.min(120, args.max_wait_seconds ?? 60)) * 1000;
64
+ const inv = await postJson(`${FD_API}/v1/agents/${SLUG}/invoke`, { task: args.lead_description });
65
+ if (inv.status === 401)
66
+ return { status: 'error', agent: SLUG, message: 'Invalid FD_API_KEY (401). Check the key in the MCP server env.' };
67
+ if (!inv.json?.task_id)
68
+ return { status: 'error', agent: SLUG, message: `Invoke failed (HTTP ${inv.status}): ${inv.json?.error || inv.json?.note || 'no task_id'}` };
69
+ const taskId = inv.json.task_id;
70
+ const start = Date.now();
71
+ let intervalMs = 2500;
72
+ while (Date.now() - start < maxWaitMs) {
73
+ await sleep(intervalMs);
74
+ const poll = await getJson(`${FD_API}/v1/agents/${SLUG}/result/${encodeURIComponent(taskId)}`);
75
+ const d = poll.json || {};
76
+ const status = d.status || d.outcome;
77
+ if (status === 'completed' || status === 'succeeded' || d.ok === true) {
78
+ return {
79
+ status: 'completed', agent: SLUG, task_id: taskId, output: d.output, charged_pence: d.charged_pence,
80
+ proof_id: d.proof_id || taskId,
81
+ verify_proof_hint: `Verify trustlessly: call forcedream_verify_proof with task_id "${d.proof_id || taskId}". The signature proves authenticity without trusting ForceDream.`,
82
+ message: `Completed. Charged ${d.charged_pence}p. Cryptographically proven (proof_id ${d.proof_id || taskId}).`,
83
+ };
84
+ }
85
+ if (status === 'charge_failed')
86
+ 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.` };
87
+ if (status === 'failed' || status === 'dead_letter')
88
+ return { status: 'error', agent: SLUG, task_id: taskId, message: `Task ${status}: ${d.reason || d.last_error || 'unknown'}` };
89
+ intervalMs = Math.min(intervalMs + 1000, 6000);
90
+ }
91
+ 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.` };
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcedream/mcp-server",
3
- "version": "0.7.0",
3
+ "version": "0.9.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",