@forcedream/mcp-server 0.8.0 → 0.9.1

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/dist/index.js CHANGED
@@ -8,6 +8,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
10
  import { checkFraud, checkFraudSchema, generateEmbedding, generateEmbeddingSchema, marketQuote, marketQuoteSchema } from './direct_tools.js';
11
+ import { scoreLead, scoreLeadSchema } from './score_lead.js';
11
12
  import { searchReliability, searchReliabilitySchema } from './search_reliability.js';
12
13
  import { searchCosts, searchCostsSchema } from './search_costs.js';
13
14
  import { searchProviders } from './search_providers.js';
@@ -60,7 +61,10 @@ server.registerTool('forcedream_invoke_agent', {
60
61
  title: 'Invoke a ForceDream agent',
61
62
  description: 'Invoke a ForceDream agent to do real work. SPENDS your balance — requires FD_API_KEY in the server env. ' +
62
63
  'Returns the output, what you were charged, and a proof_id you can verify with forcedream_verify_proof. ' +
63
- 'Handles honest declines (charged 0) and insufficient balance gracefully. Invokes once; never double-charges.',
64
+ 'Handles honest declines (charged 0) and insufficient balance gracefully. Invokes once; never double-charges. ' +
65
+ 'Use this for any agent WITHOUT a dedicated tool. For security-scan-v1, data-extract-v1, or lead-score-v1 ' +
66
+ 'specifically, prefer forcedream_security_scan, forcedream_extract_data, or forcedream_score_lead instead -- ' +
67
+ 'same underlying agents, simpler input shape.',
64
68
  inputSchema: invokeAgentSchema,
65
69
  }, async ({ agent_slug, task, max_wait_seconds }) => {
66
70
  try {
@@ -110,6 +114,26 @@ server.registerTool('forcedream_extract_data', {
110
114
  return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
111
115
  }
112
116
  });
117
+ // forcedream_score_lead — dedicated, named tool for lead-score-v1 specifically.
118
+ // Spends balance (needs FD_API_KEY). Same real invoke/poll pattern as forcedream_extract_data,
119
+ // fixed to this one agent so a caller doesn't need to know the generic agent_slug pattern.
120
+ server.registerTool('forcedream_score_lead', {
121
+ title: 'Score a sales lead',
122
+ description: 'Score sales leads hot/warm/cold with weighted signals and a recommended next action — grounded in real, ' +
123
+ 'live verification. Cross-references detected companies, domains, and locations against 8 real sources: ' +
124
+ 'Wikidata, UK Companies House, EU VIES VAT validation, postcodes.io, Google PageSpeed Insights, ' +
125
+ 'OpenStreetMap Nominatim, DNS/MX records, and live HTTP checks. Global by design — 5 sources work for any ' +
126
+ 'lead worldwide; 3 regional ones (UK/EU) apply only when genuinely detected. SPENDS your balance — requires FD_API_KEY.',
127
+ inputSchema: scoreLeadSchema,
128
+ }, async ({ lead_description, max_wait_seconds }) => {
129
+ try {
130
+ const result = await scoreLead({ lead_description, max_wait_seconds });
131
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
132
+ }
133
+ catch (e) {
134
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'error', error: e.message }, null, 2) }], isError: true };
135
+ }
136
+ });
113
137
  // forcedream_check_fraud — real, direct, synchronous call (no polling). Spends balance
114
138
  // (needs FD_API_KEY). Uses the new, dedicated fd_live_-accepting backend endpoint.
115
139
  server.registerTool('forcedream_check_fraud', {
@@ -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.8.0",
3
+ "version": "0.9.1",
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",