fodda-mcp 1.32.1 → 1.46.31
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.md +14 -16
- package/dist/a2aHandler.d.ts +33 -3
- package/dist/a2aHandler.d.ts.map +1 -1
- package/dist/a2aHandler.js +189 -36
- package/dist/a2aHandler.js.map +1 -1
- package/dist/agentFacts.d.ts +21 -0
- package/dist/agentFacts.d.ts.map +1 -0
- package/dist/agentFacts.js +72 -0
- package/dist/agentFacts.js.map +1 -0
- package/dist/agents/fodda-researcher/index.d.ts +6 -0
- package/dist/agents/fodda-researcher/index.d.ts.map +1 -1
- package/dist/agents/fodda-researcher/index.js +18 -0
- package/dist/agents/fodda-researcher/index.js.map +1 -1
- package/dist/agents/fodda-researcher/skills.d.ts +1 -1
- package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
- package/dist/agents/fodda-researcher/skills.js +8 -3
- package/dist/agents/fodda-researcher/skills.js.map +1 -1
- package/dist/brandTemplate.d.ts +1 -1
- package/dist/brandTemplate.d.ts.map +1 -1
- package/dist/brandTemplate.js +179 -11
- package/dist/brandTemplate.js.map +1 -1
- package/dist/catalogCache.d.ts +14 -13
- package/dist/catalogCache.d.ts.map +1 -1
- package/dist/catalogCache.js +266 -67
- package/dist/catalogCache.js.map +1 -1
- package/dist/coverageRelevance.d.ts +156 -0
- package/dist/coverageRelevance.d.ts.map +1 -0
- package/dist/coverageRelevance.js +1062 -0
- package/dist/coverageRelevance.js.map +1 -0
- package/dist/deepResearch.d.ts +43 -0
- package/dist/deepResearch.d.ts.map +1 -0
- package/dist/deepResearch.js +498 -0
- package/dist/deepResearch.js.map +1 -0
- package/dist/enrichment.d.ts.map +1 -1
- package/dist/enrichment.js +51 -1
- package/dist/enrichment.js.map +1 -1
- package/dist/errorHandling.d.ts +1 -1
- package/dist/errorHandling.d.ts.map +1 -1
- package/dist/errorHandling.js +26 -6
- package/dist/errorHandling.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +548 -38
- package/dist/index.js.map +1 -1
- package/dist/linkedinEngine.d.ts +150 -0
- package/dist/linkedinEngine.d.ts.map +1 -0
- package/dist/linkedinEngine.js +696 -0
- package/dist/linkedinEngine.js.map +1 -0
- package/dist/oauthRegisterShim.d.ts +13 -0
- package/dist/oauthRegisterShim.d.ts.map +1 -0
- package/dist/oauthRegisterShim.js +83 -0
- package/dist/oauthRegisterShim.js.map +1 -0
- package/dist/pricingCache.d.ts +6 -8
- package/dist/pricingCache.d.ts.map +1 -1
- package/dist/pricingCache.js +69 -13
- package/dist/pricingCache.js.map +1 -1
- package/dist/queryCache.d.ts +2 -0
- package/dist/queryCache.d.ts.map +1 -1
- package/dist/queryCache.js +11 -2
- package/dist/queryCache.js.map +1 -1
- package/dist/resources.d.ts +27 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +126 -0
- package/dist/resources.js.map +1 -0
- package/dist/sessionTracker.d.ts +24 -2
- package/dist/sessionTracker.d.ts.map +1 -1
- package/dist/sessionTracker.js +198 -4
- package/dist/sessionTracker.js.map +1 -1
- package/dist/systemPrompt.d.ts +3 -3
- package/dist/systemPrompt.d.ts.map +1 -1
- package/dist/systemPrompt.js +81 -61
- package/dist/systemPrompt.js.map +1 -1
- package/dist/telemetry.d.ts +43 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +101 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test_analyst.js +2 -2
- package/dist/test_analyst.js.map +1 -1
- package/dist/test_book_a_call_live.d.ts +2 -0
- package/dist/test_book_a_call_live.d.ts.map +1 -0
- package/dist/test_book_a_call_live.js +148 -0
- package/dist/test_book_a_call_live.js.map +1 -0
- package/dist/test_brand_intelligence_endpoint.d.ts +2 -0
- package/dist/test_brand_intelligence_endpoint.d.ts.map +1 -0
- package/dist/test_brand_intelligence_endpoint.js +20 -0
- package/dist/test_brand_intelligence_endpoint.js.map +1 -0
- package/dist/test_coverage_relevance.d.ts +12 -0
- package/dist/test_coverage_relevance.d.ts.map +1 -0
- package/dist/test_coverage_relevance.js +121 -0
- package/dist/test_coverage_relevance.js.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.d.ts +2 -0
- package/dist/test_dcr_and_legacy_deprecation.d.ts.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.js +148 -0
- package/dist/test_dcr_and_legacy_deprecation.js.map +1 -0
- package/dist/test_financial_snapshot_render.d.ts +2 -0
- package/dist/test_financial_snapshot_render.d.ts.map +1 -0
- package/dist/test_financial_snapshot_render.js +167 -0
- package/dist/test_financial_snapshot_render.js.map +1 -0
- package/dist/test_gap_alert.d.ts +12 -0
- package/dist/test_gap_alert.d.ts.map +1 -0
- package/dist/test_gap_alert.js +43 -0
- package/dist/test_gap_alert.js.map +1 -0
- package/dist/test_list_graphs_topics.d.ts +2 -0
- package/dist/test_list_graphs_topics.d.ts.map +1 -0
- package/dist/test_list_graphs_topics.js +26 -0
- package/dist/test_list_graphs_topics.js.map +1 -0
- package/dist/test_live_endpoint_runner.d.ts +2 -0
- package/dist/test_live_endpoint_runner.d.ts.map +1 -0
- package/dist/test_live_endpoint_runner.js +65 -0
- package/dist/test_live_endpoint_runner.js.map +1 -0
- package/dist/test_local_list_graphs.d.ts +2 -0
- package/dist/test_local_list_graphs.d.ts.map +1 -0
- package/dist/test_local_list_graphs.js +53 -0
- package/dist/test_local_list_graphs.js.map +1 -0
- package/dist/test_next_moves.d.ts +2 -0
- package/dist/test_next_moves.d.ts.map +1 -0
- package/dist/test_next_moves.js +349 -0
- package/dist/test_next_moves.js.map +1 -0
- package/dist/test_next_moves_transcripts.d.ts +2 -0
- package/dist/test_next_moves_transcripts.d.ts.map +1 -0
- package/dist/test_next_moves_transcripts.js +413 -0
- package/dist/test_next_moves_transcripts.js.map +1 -0
- package/dist/test_nike_cmo_alias.d.ts +2 -0
- package/dist/test_nike_cmo_alias.d.ts.map +1 -0
- package/dist/test_nike_cmo_alias.js +76 -0
- package/dist/test_nike_cmo_alias.js.map +1 -0
- package/dist/test_session_next_moves_telemetry.d.ts +2 -0
- package/dist/test_session_next_moves_telemetry.d.ts.map +1 -0
- package/dist/test_session_next_moves_telemetry.js +100 -0
- package/dist/test_session_next_moves_telemetry.js.map +1 -0
- package/dist/test_token_resolution.d.ts +2 -0
- package/dist/test_token_resolution.d.ts.map +1 -0
- package/dist/test_token_resolution.js +82 -0
- package/dist/test_token_resolution.js.map +1 -0
- package/dist/toolHandlers.d.ts +7 -3
- package/dist/toolHandlers.d.ts.map +1 -1
- package/dist/toolHandlers.js +1678 -364
- package/dist/toolHandlers.js.map +1 -1
- package/dist/tools.d.ts +7 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +12 -3
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +9 -46
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/verify_tools_endpoint.js +4 -3
- package/dist/verify_tools_endpoint.js.map +1 -1
- package/package.json +5 -2
- package/server.json +16 -3
- package/dist/scripts/simulate_gemini.d.ts +0 -2
- package/dist/scripts/simulate_gemini.d.ts.map +0 -1
- package/dist/scripts/simulate_gemini.js +0 -100
- package/dist/scripts/simulate_gemini.js.map +0 -1
- package/dist/scripts/verify_registry.d.ts +0 -2
- package/dist/scripts/verify_registry.d.ts.map +0 -1
- package/dist/scripts/verify_registry.js +0 -77
- package/dist/scripts/verify_registry.js.map +0 -1
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -332
- package/dist/src/index.js.map +0 -1
- package/dist/src/tools.d.ts +0 -12
- package/dist/src/tools.d.ts.map +0 -1
- package/dist/src/tools.js +0 -104
- package/dist/src/tools.js.map +0 -1
- package/dist/src/types.d.ts +0 -44
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -2
- package/dist/src/types.js.map +0 -1
- package/dist/src/verify.d.ts +0 -2
- package/dist/src/verify.d.ts.map +0 -1
- package/dist/src/verify.js +0 -47
- package/dist/src/verify.js.map +0 -1
- package/dist/src/verify_hmac.d.ts +0 -2
- package/dist/src/verify_hmac.d.ts.map +0 -1
- package/dist/src/verify_hmac.js +0 -106
- package/dist/src/verify_hmac.js.map +0 -1
- package/dist/src/verify_tools_endpoint.d.ts +0 -2
- package/dist/src/verify_tools_endpoint.d.ts.map +0 -1
- package/dist/src/verify_tools_endpoint.js +0 -40
- package/dist/src/verify_tools_endpoint.js.map +0 -1
- package/dist/test-waverunner-response.d.ts +0 -2
- package/dist/test-waverunner-response.d.ts.map +0 -1
- package/dist/test-waverunner-response.js +0 -20
- package/dist/test-waverunner-response.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { cacheGet, cacheSet, getCacheStats } from './queryCache.js';
|
|
|
15
15
|
import { MCP_SERVER_VERSION } from './tools.js';
|
|
16
16
|
import { createServer } from './toolHandlers.js';
|
|
17
17
|
import { registerA2ARoute } from './a2aHandler.js';
|
|
18
|
+
import { registerAgentFactsRoute } from './agentFacts.js';
|
|
19
|
+
import { getTelemetryStats, recordFeedbackEntry } from './telemetry.js';
|
|
20
|
+
import { handleOauthRegister } from './oauthRegisterShim.js';
|
|
18
21
|
import * as dotenv from 'dotenv';
|
|
19
22
|
dotenv.config();
|
|
20
23
|
// ── Global crash guards ──
|
|
@@ -29,8 +32,16 @@ process.on('uncaughtException', (err) => {
|
|
|
29
32
|
console.error('[uncaughtException] kept process alive:', err?.stack || err?.message || err);
|
|
30
33
|
});
|
|
31
34
|
const API_BASE_URL = process.env.FODDA_API_URL || 'https://api.fodda.ai';
|
|
35
|
+
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'https://www.fodda.ai';
|
|
32
36
|
const app = express();
|
|
33
37
|
app.use(express.json({ limit: '512kb' }));
|
|
38
|
+
// Strip api_key from request logging globally before anything hits Cloud Run logs
|
|
39
|
+
app.use((req, _res, next) => {
|
|
40
|
+
if (req.url && req.url.includes('api_key=')) {
|
|
41
|
+
req.url = req.url.replace(/api_key=[^&]+/g, 'api_key=REDACTED');
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
});
|
|
34
45
|
// CORS — minimal, matching test server
|
|
35
46
|
app.use((req, res, next) => {
|
|
36
47
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -41,18 +52,315 @@ app.use((req, res, next) => {
|
|
|
41
52
|
return res.status(204).end();
|
|
42
53
|
next();
|
|
43
54
|
});
|
|
44
|
-
//
|
|
55
|
+
// Deprecation middleware for legacy URL parameters (?api_key=... / ?user_id=...)
|
|
56
|
+
const LEGACY_DEPRECATION_PATHS = [
|
|
57
|
+
'/sse', '/mcp', '/messages', '/copilot',
|
|
58
|
+
'/brand-intelligence', '/topic-research', '/deep-research',
|
|
59
|
+
'/earnings-intelligence', '/expert-consult'
|
|
60
|
+
];
|
|
61
|
+
app.use(LEGACY_DEPRECATION_PATHS, (req, res, next) => {
|
|
62
|
+
if (req.query.api_key !== undefined || req.query.user_id !== undefined) {
|
|
63
|
+
const message = 'Fodda: this connection URL is outdated. Get your new MCP URL at https://app.fodda.ai (Account → MCP Integration) and update your connector.';
|
|
64
|
+
const accept = req.headers['accept'] || '';
|
|
65
|
+
const contentType = req.headers['content-type'] || '';
|
|
66
|
+
const isJson = accept.includes('application/json') || contentType.includes('application/json') || (req.method === 'POST' && req.body && typeof req.body === 'object');
|
|
67
|
+
if (isJson && req.method === 'POST') {
|
|
68
|
+
const bodyId = (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null;
|
|
69
|
+
return res.status(401).json({
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
error: {
|
|
72
|
+
code: -32001,
|
|
73
|
+
message,
|
|
74
|
+
data: { docs: 'https://fodda.ai/platform-integration-anthropic-claude' }
|
|
75
|
+
},
|
|
76
|
+
id: bodyId,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
80
|
+
return res.status(401).send(`${message}\n`);
|
|
81
|
+
}
|
|
82
|
+
next();
|
|
83
|
+
});
|
|
84
|
+
// OAuth discovery endpoints — Clerk-backed OAuth for the MCP Connectors Directory.
|
|
85
|
+
// Discovery advertises mcp.fodda.ai as authorization server metadata host and points
|
|
86
|
+
// registration_endpoint to /oauth/register shim for DCR openid scope injection.
|
|
87
|
+
const CLERK_ISSUER = process.env.CLERK_ISSUER_URL || 'https://clerk.fodda.ai';
|
|
45
88
|
app.get('/.well-known/oauth-protected-resource', (_req, res) => {
|
|
46
|
-
res.status(
|
|
89
|
+
res.status(200).json({
|
|
90
|
+
resource: getServiceUrl(),
|
|
91
|
+
authorization_servers: [getServiceUrl()],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
app.get('/.well-known/oauth-protected-resource/mcp', (_req, res) => {
|
|
95
|
+
res.status(200).json({
|
|
96
|
+
resource: `${getServiceUrl()}/mcp`,
|
|
97
|
+
authorization_servers: [getServiceUrl()],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
let cachedClerkMetadata = null;
|
|
101
|
+
let lastClerkFetch = 0;
|
|
102
|
+
app.get('/.well-known/oauth-authorization-server', async (_req, res) => {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
if (!cachedClerkMetadata || now - lastClerkFetch > 5 * 60 * 1000) {
|
|
105
|
+
try {
|
|
106
|
+
const resp = await axios.get(`${CLERK_ISSUER}/.well-known/oauth-authorization-server`, { timeout: 5000 });
|
|
107
|
+
if (resp.status === 200 && resp.data) {
|
|
108
|
+
cachedClerkMetadata = resp.data;
|
|
109
|
+
lastClerkFetch = now;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error('[oauth-discovery] Unable to refresh Clerk OAuth metadata:', err.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const serviceUrl = getServiceUrl();
|
|
117
|
+
const metadata = {
|
|
118
|
+
authorization_endpoint: `${CLERK_ISSUER}/oauth/authorize`,
|
|
119
|
+
token_endpoint: `${CLERK_ISSUER}/oauth/token`,
|
|
120
|
+
revocation_endpoint: `${CLERK_ISSUER}/oauth/token/revoke`,
|
|
121
|
+
jwks_uri: `${CLERK_ISSUER}/.well-known/jwks.json`,
|
|
122
|
+
response_types_supported: ['code'],
|
|
123
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
124
|
+
token_endpoint_auth_methods_supported: ['client_secret_basic', 'none', 'client_secret_post'],
|
|
125
|
+
scopes_supported: ['openid', 'profile', 'email', 'public_metadata', 'private_metadata', 'offline_access', 'user:org:read'],
|
|
126
|
+
subject_types_supported: ['public'],
|
|
127
|
+
id_token_signing_alg_values_supported: ['RS256'],
|
|
128
|
+
claims_supported: ['sub', 'iss', 'aud', 'exp', 'iat', 'email', 'name', 'org_id'],
|
|
129
|
+
service_documentation: 'https://clerk.com/docs/oauth/scoped-access',
|
|
130
|
+
ui_locales_supported: ['en'],
|
|
131
|
+
op_tos_uri: 'https://clerk.com/legal/standard-terms',
|
|
132
|
+
code_challenge_methods_supported: ['S256'],
|
|
133
|
+
authorization_response_iss_parameter_supported: true,
|
|
134
|
+
...(cachedClerkMetadata || {}),
|
|
135
|
+
issuer: CLERK_ISSUER,
|
|
136
|
+
registration_endpoint: `${serviceUrl}/oauth/register`,
|
|
137
|
+
};
|
|
138
|
+
res.status(200).json(metadata);
|
|
47
139
|
});
|
|
48
|
-
app.
|
|
49
|
-
|
|
140
|
+
app.post(['/oauth/register', '/register'], handleOauthRegister);
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// .well-known MCP Server Discovery Card & Pricing Metadata Tier
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
export const OFFERING_SCOPED_TOOLS = {
|
|
145
|
+
'brand-intelligence': [
|
|
146
|
+
'get_capabilities',
|
|
147
|
+
'brand_tracker',
|
|
148
|
+
'search_graph',
|
|
149
|
+
'read_url',
|
|
150
|
+
'get_supplemental_context',
|
|
151
|
+
'check_supplemental_status',
|
|
152
|
+
'get_evidence',
|
|
153
|
+
'get_node',
|
|
154
|
+
'get_neighbors',
|
|
155
|
+
'get_label_values',
|
|
156
|
+
'list_graphs',
|
|
157
|
+
'get_my_account',
|
|
158
|
+
'generate_visual',
|
|
159
|
+
],
|
|
160
|
+
'topic-research': [
|
|
161
|
+
'get_capabilities',
|
|
162
|
+
'search_graph',
|
|
163
|
+
'search_statistics',
|
|
164
|
+
'search_insights',
|
|
165
|
+
'get_validated_trends',
|
|
166
|
+
'get_evidence',
|
|
167
|
+
'get_node',
|
|
168
|
+
'get_neighbors',
|
|
169
|
+
'get_label_values',
|
|
170
|
+
'list_graphs',
|
|
171
|
+
'get_my_account',
|
|
172
|
+
'generate_visual',
|
|
173
|
+
'read_url',
|
|
174
|
+
'get_supplemental_context',
|
|
175
|
+
'check_supplemental_status',
|
|
176
|
+
],
|
|
177
|
+
'deep-research': [
|
|
178
|
+
'get_capabilities',
|
|
179
|
+
'deep_research_topic',
|
|
180
|
+
'check_research_status',
|
|
181
|
+
'search_graph',
|
|
182
|
+
'get_evidence',
|
|
183
|
+
'get_node',
|
|
184
|
+
'get_neighbors',
|
|
185
|
+
'get_label_values',
|
|
186
|
+
'list_graphs',
|
|
187
|
+
'get_my_account',
|
|
188
|
+
'generate_visual',
|
|
189
|
+
'read_url',
|
|
190
|
+
'get_supplemental_context',
|
|
191
|
+
'check_supplemental_status',
|
|
192
|
+
],
|
|
193
|
+
'earnings-intelligence': [
|
|
194
|
+
'get_capabilities',
|
|
195
|
+
'get_earnings_intelligence',
|
|
196
|
+
'get_earnings_divergence',
|
|
197
|
+
'get_company_earnings',
|
|
198
|
+
'get_validated_trends',
|
|
199
|
+
'search_graph',
|
|
200
|
+
'get_evidence',
|
|
201
|
+
'get_node',
|
|
202
|
+
'get_neighbors',
|
|
203
|
+
'get_label_values',
|
|
204
|
+
'list_graphs',
|
|
205
|
+
'get_my_account',
|
|
206
|
+
'generate_visual',
|
|
207
|
+
],
|
|
208
|
+
'expert-consult': [
|
|
209
|
+
'get_capabilities',
|
|
210
|
+
'consult_analyst',
|
|
211
|
+
'consult_human_agent',
|
|
212
|
+
'list_analysts',
|
|
213
|
+
'request_deliverable',
|
|
214
|
+
'check_deliverable_status',
|
|
215
|
+
'search_graph',
|
|
216
|
+
'get_evidence',
|
|
217
|
+
'get_node',
|
|
218
|
+
'get_neighbors',
|
|
219
|
+
'get_label_values',
|
|
220
|
+
'list_graphs',
|
|
221
|
+
'get_my_account',
|
|
222
|
+
'generate_visual',
|
|
223
|
+
],
|
|
224
|
+
'copilot': [
|
|
225
|
+
'get_capabilities',
|
|
226
|
+
'search_graph',
|
|
227
|
+
'search_statistics',
|
|
228
|
+
'search_insights',
|
|
229
|
+
'get_validated_trends',
|
|
230
|
+
'brand_tracker',
|
|
231
|
+
'get_company_earnings',
|
|
232
|
+
'deep_research_topic',
|
|
233
|
+
'check_research_status',
|
|
234
|
+
'consult_analyst',
|
|
235
|
+
'consult_human_agent',
|
|
236
|
+
'list_analysts',
|
|
237
|
+
'get_evidence',
|
|
238
|
+
'list_graphs',
|
|
239
|
+
'get_my_account',
|
|
240
|
+
'read_url',
|
|
241
|
+
'get_supplemental_context',
|
|
242
|
+
'check_supplemental_status',
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
const OFFERING_CARD_METADATA = {
|
|
246
|
+
'brand-intelligence': {
|
|
247
|
+
name: 'ai.fodda/brand-intelligence',
|
|
248
|
+
title: 'Fodda Brand Intelligence',
|
|
249
|
+
description: 'Brand health & trend footprint across PSFK expert graphs with citable sources, not web summaries.',
|
|
250
|
+
},
|
|
251
|
+
'topic-research': {
|
|
252
|
+
name: 'ai.fodda/topic-research',
|
|
253
|
+
title: 'Fodda Topic & Trend Research',
|
|
254
|
+
description: 'Trend, statistics & insight search across PSFK expert graphs with citable sources, not web summaries.',
|
|
255
|
+
},
|
|
256
|
+
'deep-research': {
|
|
257
|
+
name: 'ai.fodda/deep-research',
|
|
258
|
+
title: 'Fodda Deep Research',
|
|
259
|
+
description: 'Autonomous deep research reports merging PSFK knowledge graphs & web validation with citable sources.',
|
|
260
|
+
},
|
|
261
|
+
'earnings-intelligence': {
|
|
262
|
+
name: 'ai.fodda/earnings-intelligence',
|
|
263
|
+
title: 'Fodda Earnings Intelligence',
|
|
264
|
+
description: 'Cross-company earnings trends, executive divergence & ticker analysis with citable sources.',
|
|
265
|
+
},
|
|
266
|
+
'expert-consult': {
|
|
267
|
+
name: 'ai.fodda/expert-consult',
|
|
268
|
+
title: 'Fodda Synthetic Expert Consult',
|
|
269
|
+
description: 'Consult synthetic industry experts grounded in PSFK trend graphs with citable analyst perspectives.',
|
|
270
|
+
},
|
|
271
|
+
'copilot': {
|
|
272
|
+
name: 'ai.fodda/copilot',
|
|
273
|
+
title: 'Fodda Copilot Market Intelligence',
|
|
274
|
+
description: 'Curated market intelligence, trend insights & expert evidence for Microsoft Copilot Studio agents.',
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
app.get([
|
|
278
|
+
'/.well-known/mcp-server.json', '/.well-known/mcp',
|
|
279
|
+
'/.well-known/brand-intelligence', '/.well-known/brand-intelligence.json',
|
|
280
|
+
'/.well-known/topic-research', '/.well-known/topic-research.json',
|
|
281
|
+
'/.well-known/deep-research', '/.well-known/deep-research.json',
|
|
282
|
+
'/.well-known/earnings-intelligence', '/.well-known/earnings-intelligence.json',
|
|
283
|
+
'/.well-known/expert-consult', '/.well-known/expert-consult.json',
|
|
284
|
+
'/.well-known/copilot', '/.well-known/copilot.json',
|
|
285
|
+
], (req, res) => {
|
|
286
|
+
res.setHeader('Content-Type', 'application/json');
|
|
287
|
+
const matchedOffering = Object.keys(OFFERING_CARD_METADATA).find(slug => req.path.includes(slug));
|
|
288
|
+
if (matchedOffering && OFFERING_CARD_METADATA[matchedOffering]) {
|
|
289
|
+
const meta = OFFERING_CARD_METADATA[matchedOffering];
|
|
290
|
+
return res.json({
|
|
291
|
+
$schema: 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json',
|
|
292
|
+
name: meta.name,
|
|
293
|
+
title: meta.title,
|
|
294
|
+
description: meta.description,
|
|
295
|
+
websiteUrl: 'https://www.fodda.ai',
|
|
296
|
+
version: MCP_SERVER_VERSION,
|
|
297
|
+
capabilities: {
|
|
298
|
+
tools: { listChanged: false },
|
|
299
|
+
resources: { listChanged: false, subscribe: false },
|
|
300
|
+
prompts: { listChanged: false },
|
|
301
|
+
},
|
|
302
|
+
pricingTier: {
|
|
303
|
+
structure: 'free',
|
|
304
|
+
substance: 'metered',
|
|
305
|
+
tokenModel: 'Canonical token costs defined in API metering.ts ($0.50 via SPT / API credits)',
|
|
306
|
+
},
|
|
307
|
+
resourceSchemes: [
|
|
308
|
+
'fodda://expert/{slug}/insight/{id}',
|
|
309
|
+
'fodda://graph/{vertical}/trend/{slug}',
|
|
310
|
+
],
|
|
311
|
+
endpoints: {
|
|
312
|
+
mcpStreamableHttp: `${getServiceUrl()}/${matchedOffering}`,
|
|
313
|
+
mcpSse: `${getServiceUrl()}/sse`,
|
|
314
|
+
telemetry: `${getServiceUrl()}/telemetry`,
|
|
315
|
+
feedback: `${getServiceUrl()}/v1/feedback`,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
res.json({
|
|
320
|
+
$schema: 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json',
|
|
321
|
+
name: 'ai.fodda/mcp-server',
|
|
322
|
+
title: 'Fodda Knowledge Graphs',
|
|
323
|
+
description: 'Expert-curated knowledge, brand, research & earnings intelligence — 46 tools, 220+ graphs.',
|
|
324
|
+
websiteUrl: 'https://www.fodda.ai',
|
|
325
|
+
version: MCP_SERVER_VERSION,
|
|
326
|
+
capabilities: {
|
|
327
|
+
tools: { listChanged: false },
|
|
328
|
+
resources: { listChanged: false, subscribe: false },
|
|
329
|
+
prompts: { listChanged: false },
|
|
330
|
+
},
|
|
331
|
+
pricingTier: {
|
|
332
|
+
structure: 'free',
|
|
333
|
+
substance: 'metered',
|
|
334
|
+
tokenModel: 'Canonical token costs defined in API metering.ts ($0.50 via SPT / API credits)',
|
|
335
|
+
},
|
|
336
|
+
resourceSchemes: [
|
|
337
|
+
'fodda://expert/{slug}/insight/{id}',
|
|
338
|
+
'fodda://graph/{vertical}/trend/{slug}',
|
|
339
|
+
],
|
|
340
|
+
endpoints: {
|
|
341
|
+
mcpStreamableHttp: `${getServiceUrl()}/mcp`,
|
|
342
|
+
mcpSse: `${getServiceUrl()}/sse`,
|
|
343
|
+
telemetry: `${getServiceUrl()}/telemetry`,
|
|
344
|
+
feedback: `${getServiceUrl()}/v1/feedback`,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
50
347
|
});
|
|
51
|
-
|
|
52
|
-
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Telemetry & Error-Rate Instrumentation Endpoints
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
app.get(['/telemetry', '/v1/telemetry'], (_req, res) => {
|
|
352
|
+
res.setHeader('Content-Type', 'application/json');
|
|
353
|
+
res.json(getTelemetryStats());
|
|
53
354
|
});
|
|
54
|
-
app.post('/
|
|
55
|
-
|
|
355
|
+
app.post('/v1/feedback', (req, res) => {
|
|
356
|
+
const { category, feedback, user_email, user } = req.body || {};
|
|
357
|
+
if (!feedback) {
|
|
358
|
+
return res.status(400).json({ error: 'Missing required field: feedback' });
|
|
359
|
+
}
|
|
360
|
+
const userLabel = user_email || user || 'anonymous HTTP user';
|
|
361
|
+
const catLabel = category || 'general';
|
|
362
|
+
recordFeedbackEntry(catLabel, feedback, userLabel);
|
|
363
|
+
res.json({ status: 'LOGGED', message: 'Feedback recorded successfully.' });
|
|
56
364
|
});
|
|
57
365
|
// ---------------------------------------------------------------------------
|
|
58
366
|
// Widget cache — stores pre-rendered HTML for browser access
|
|
@@ -121,14 +429,8 @@ setInterval(() => {
|
|
|
121
429
|
function getServiceUrl() {
|
|
122
430
|
if (process.env.FODDA_SERVICE_URL)
|
|
123
431
|
return process.env.FODDA_SERVICE_URL;
|
|
124
|
-
if (process.env.K_SERVICE) {
|
|
125
|
-
|
|
126
|
-
// Requires CLOUD_RUN_REGION env var (e.g. 'uk') to be set.
|
|
127
|
-
const region = process.env.CLOUD_RUN_REGION || 'uk';
|
|
128
|
-
const revHash = process.env.K_REVISION?.split('-').pop();
|
|
129
|
-
if (!revHash)
|
|
130
|
-
console.error('[getServiceUrl] WARNING: K_REVISION not set — widget URLs may be wrong');
|
|
131
|
-
return `https://${process.env.K_SERVICE}-${revHash || 'unknown'}-${region}.a.run.app`;
|
|
432
|
+
if (process.env.NODE_ENV === 'production' || process.env.K_SERVICE) {
|
|
433
|
+
return 'https://mcp.fodda.ai';
|
|
132
434
|
}
|
|
133
435
|
return `http://localhost:${process.env.PORT || 8080}`;
|
|
134
436
|
}
|
|
@@ -165,21 +467,26 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
165
467
|
// HMAC sign the request
|
|
166
468
|
const secret = process.env.FODDA_MCP_SECRET;
|
|
167
469
|
if (secret) {
|
|
168
|
-
const payload = (method === 'POST' || method === 'PATCH')
|
|
169
|
-
? timestamp + '.' + JSON.stringify(body)
|
|
470
|
+
const payload = (method === 'POST' || method === 'PATCH')
|
|
471
|
+
? timestamp + '.' + JSON.stringify(body ?? {})
|
|
170
472
|
: timestamp + '.' + path;
|
|
171
473
|
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
172
474
|
headers['X-Fodda-Signature'] = signature;
|
|
173
475
|
}
|
|
174
|
-
const
|
|
476
|
+
const baseUrl = path.startsWith('/api/') ? WEBSITE_BASE_URL : API_BASE_URL;
|
|
477
|
+
const url = `${baseUrl}${path}`;
|
|
175
478
|
// Base timeout: 30s aligns with MCP client expectations.
|
|
176
|
-
// Extended to
|
|
177
|
-
const AXIOS_TIMEOUT_MS =
|
|
479
|
+
// Extended to 90s for analyst and human agent consults, and 35s for supplemental.
|
|
480
|
+
const AXIOS_TIMEOUT_MS = /(\/analysts\/consult|\/human-agents\/consult)/.test(path)
|
|
481
|
+
? 90000
|
|
482
|
+
: /\/supplemental\//.test(path)
|
|
483
|
+
? 35000
|
|
484
|
+
: 30000;
|
|
178
485
|
const response = method === 'GET'
|
|
179
486
|
? await axios.get(url, { headers, timeout: AXIOS_TIMEOUT_MS })
|
|
180
487
|
: method === 'PATCH'
|
|
181
|
-
? await axios.patch(url, body, { headers, timeout: AXIOS_TIMEOUT_MS })
|
|
182
|
-
: await axios.post(url, body, { headers, timeout: AXIOS_TIMEOUT_MS });
|
|
488
|
+
? await axios.patch(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS })
|
|
489
|
+
: await axios.post(url, body ?? {}, { headers, timeout: AXIOS_TIMEOUT_MS });
|
|
183
490
|
// ── Cache store ──
|
|
184
491
|
cacheSet(method, path, body, response.data);
|
|
185
492
|
// ── Inject upstream usage warning headers into response data ──
|
|
@@ -250,25 +557,95 @@ async function validateSpt(spt) {
|
|
|
250
557
|
async function waverunnerRequest(interactionType, tokenCost, userApiKey, userId, waverunnerPayload) {
|
|
251
558
|
// ── Pre-check credits ──
|
|
252
559
|
// (sk_trial_ pre-check removed — new trial accounts are metered server-side by the API)
|
|
253
|
-
// ── Call Waverunner via Gemini SDK ──
|
|
254
|
-
const geminiKey = process.env.GEMINI_API_KEY;
|
|
560
|
+
// ── Call Waverunner via Gemini SDK (generateContent) ──
|
|
561
|
+
const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY;
|
|
255
562
|
if (!geminiKey) {
|
|
256
563
|
throw new Error('GEMINI_API_KEY environment variable is required for Waverunner calls.');
|
|
257
564
|
}
|
|
258
565
|
const { GoogleGenAI } = await import('@google/genai');
|
|
259
566
|
const ai = new GoogleGenAI({ apiKey: geminiKey });
|
|
260
|
-
const
|
|
567
|
+
const model = waverunnerPayload.model || 'gemini-2.5-flash';
|
|
568
|
+
const systemInstruction = waverunnerPayload.system_instruction;
|
|
569
|
+
const inputParts = Array.isArray(waverunnerPayload.input)
|
|
570
|
+
? waverunnerPayload.input.map((i) => i.text || '').filter(Boolean).join('\n\n')
|
|
571
|
+
: (typeof waverunnerPayload.input === 'string' ? waverunnerPayload.input : (waverunnerPayload.prompt || ''));
|
|
572
|
+
const config = {};
|
|
573
|
+
if (systemInstruction) {
|
|
574
|
+
config.systemInstruction = systemInstruction;
|
|
575
|
+
}
|
|
576
|
+
if (waverunnerPayload.response_mime_type || waverunnerPayload.responseMimeType) {
|
|
577
|
+
config.responseMimeType = waverunnerPayload.response_mime_type || waverunnerPayload.responseMimeType;
|
|
578
|
+
}
|
|
579
|
+
if (Array.isArray(waverunnerPayload.tools) && waverunnerPayload.tools.length > 0) {
|
|
580
|
+
const toolsMapped = [];
|
|
581
|
+
let googleSearchAdded = false;
|
|
582
|
+
let urlContextAdded = false;
|
|
583
|
+
for (const t of waverunnerPayload.tools) {
|
|
584
|
+
if (t.type === 'url_context') {
|
|
585
|
+
// urlContext retrieves the page itself. It must NOT collapse into
|
|
586
|
+
// googleSearch — search cannot fetch a specific URL, which left
|
|
587
|
+
// read_url asking the model to extract a page it had no way to
|
|
588
|
+
// read, so it answered with a refusal instead of content.
|
|
589
|
+
if (!urlContextAdded) {
|
|
590
|
+
toolsMapped.push({ urlContext: {} });
|
|
591
|
+
urlContextAdded = true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else if (t.type === 'google_search') {
|
|
595
|
+
if (!googleSearchAdded) {
|
|
596
|
+
toolsMapped.push({ googleSearch: {} });
|
|
597
|
+
googleSearchAdded = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
toolsMapped.push(t);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (toolsMapped.length > 0) {
|
|
605
|
+
config.tools = toolsMapped;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const generatePromise = ai.models.generateContent({
|
|
609
|
+
model,
|
|
610
|
+
contents: [
|
|
611
|
+
{
|
|
612
|
+
role: 'user',
|
|
613
|
+
parts: [{ text: inputParts }]
|
|
614
|
+
}
|
|
615
|
+
],
|
|
616
|
+
config
|
|
617
|
+
});
|
|
618
|
+
let timeoutTimer;
|
|
619
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
620
|
+
timeoutTimer = setTimeout(() => reject(new Error('Gemini model generation timed out after 180 seconds.')), 180000);
|
|
621
|
+
});
|
|
622
|
+
let response;
|
|
623
|
+
try {
|
|
624
|
+
response = await Promise.race([generatePromise, timeoutPromise]);
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
clearTimeout(timeoutTimer);
|
|
628
|
+
}
|
|
629
|
+
const reportText = response.text || '';
|
|
630
|
+
const groundingChunks = response.candidates?.[0]?.groundingMetadata?.groundingChunks || [];
|
|
631
|
+
// Surfaced so URL-reading callers can distinguish "page fetched" from a
|
|
632
|
+
// model refusal — a refusal is non-empty text and otherwise looks like success.
|
|
633
|
+
const urlContextMetadata = response.candidates?.[0]?.urlContextMetadata || null;
|
|
261
634
|
// ── Post-decrement (paid accounts) ──
|
|
262
635
|
foddaRequest('POST', '/v1/research/meter', userApiKey, userId, {
|
|
263
636
|
type: interactionType,
|
|
264
637
|
billable_units: tokenCost,
|
|
265
638
|
}).catch(err => console.error('[waverunnerRequest] Metering failed:', err.message));
|
|
266
|
-
return
|
|
639
|
+
return {
|
|
640
|
+
outputs: [{ type: 'text', text: reportText }],
|
|
641
|
+
groundingMetadata: { groundingChunks },
|
|
642
|
+
urlContextMetadata
|
|
643
|
+
};
|
|
267
644
|
}
|
|
268
645
|
// ---------------------------------------------------------------------------
|
|
269
646
|
// Browser Landing Page — catch humans who paste the MCP URL into a browser
|
|
270
647
|
// ---------------------------------------------------------------------------
|
|
271
|
-
app.get('/mcp', (req, res, next) => {
|
|
648
|
+
app.get(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult', '/copilot'], (req, res, next) => {
|
|
272
649
|
// Only intercept browser requests (Accept: text/html).
|
|
273
650
|
// MCP SDK clients send application/json or text/event-stream, so they
|
|
274
651
|
// fall through to the app.all('/mcp') transport handler below.
|
|
@@ -518,13 +895,58 @@ app.get('/mcp', (req, res, next) => {
|
|
|
518
895
|
</body>
|
|
519
896
|
</html>`);
|
|
520
897
|
});
|
|
898
|
+
const tokenCache = new Map();
|
|
899
|
+
const TOKEN_CACHE_TTL_MS = 60 * 1000; // 1 minute TTL
|
|
900
|
+
export async function resolveMcpToken(token, websiteBaseUrl) {
|
|
901
|
+
const cached = tokenCache.get(token);
|
|
902
|
+
if (cached && (Date.now() - cached.fetchedAt < TOKEN_CACHE_TTL_MS)) {
|
|
903
|
+
return cached;
|
|
904
|
+
}
|
|
905
|
+
const secret = process.env.FODDA_MCP_SECRET || process.env.ONBOARD_SECRET || '';
|
|
906
|
+
if (!secret) {
|
|
907
|
+
console.warn('[token-resolver] Neither FODDA_MCP_SECRET nor ONBOARD_SECRET is set in environment; sending token resolution request without X-Fodda-Mcp-Secret header.');
|
|
908
|
+
}
|
|
909
|
+
const headers = {};
|
|
910
|
+
if (secret) {
|
|
911
|
+
headers['X-Fodda-Mcp-Secret'] = secret;
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
const response = await axios.get(`${websiteBaseUrl}/api/mcp-tokens/${token}`, {
|
|
915
|
+
headers,
|
|
916
|
+
timeout: 5000,
|
|
917
|
+
});
|
|
918
|
+
const data = response.data;
|
|
919
|
+
if (!data.apiKey || !data.email) {
|
|
920
|
+
throw new Error('Invalid token resolution payload');
|
|
921
|
+
}
|
|
922
|
+
const val = {
|
|
923
|
+
apiKey: data.apiKey,
|
|
924
|
+
email: data.email,
|
|
925
|
+
name: data.name || '',
|
|
926
|
+
analystId: data.analystId || '',
|
|
927
|
+
fetchedAt: Date.now()
|
|
928
|
+
};
|
|
929
|
+
tokenCache.set(token, val);
|
|
930
|
+
return val;
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
if (axios.isAxiosError(err) && err.response && (err.response.status === 401 || err.response.status === 403)) {
|
|
934
|
+
throw new Error(`Token resolution request unauthorized (HTTP ${err.response.status}). Check FODDA_MCP_SECRET / ONBOARD_SECRET configuration.`);
|
|
935
|
+
}
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
521
939
|
// ---------------------------------------------------------------------------
|
|
522
940
|
// MCP Transport Handler
|
|
523
941
|
// ---------------------------------------------------------------------------
|
|
524
|
-
app.all('/mcp', async (req, res) => {
|
|
942
|
+
app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult', '/copilot', '/c/:token'], async (req, res) => {
|
|
525
943
|
try {
|
|
526
944
|
const sessionId = req.headers['mcp-session-id'];
|
|
527
945
|
let transport;
|
|
946
|
+
// Determine offering slug and tool filtering from URL path
|
|
947
|
+
const rawPath = req.path || '';
|
|
948
|
+
const offeringSlug = (rawPath.replace(/^\//, '').split('/')[0] || '');
|
|
949
|
+
const allowedTools = OFFERING_SCOPED_TOOLS[offeringSlug];
|
|
528
950
|
// SPT (anonymous Shared Payment Token) detection — MUST precede api-key
|
|
529
951
|
// extraction so an `spt_xxx` Bearer is never treated as an API key.
|
|
530
952
|
const rawAuth = req.headers['authorization']?.toString() || '';
|
|
@@ -533,30 +955,116 @@ app.all('/mcp', async (req, res) => {
|
|
|
533
955
|
|| (/^Bearer\s+spt_/i.test(rawAuth) ? rawAuth.replace(/^Bearer\s+/i, '') : '')
|
|
534
956
|
|| '';
|
|
535
957
|
const isSpt = !!spt;
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
|
|
958
|
+
const rawBearer = !isSpt ? rawAuth.replace(/^Bearer\s+/i, '').trim() : '';
|
|
959
|
+
// Clerk OAuth tokens may be JWTs (eyJ...), opaque tokens (oat_...), session tokens (sess_...), or custom OAuth tokens.
|
|
960
|
+
// If it's not a standard sk_live_ / sk_trial_ key, send it to clerk-resolve.
|
|
961
|
+
const isClerkJwt = !isSpt && rawBearer.length > 0 && !rawBearer.startsWith('sk_live_') && !rawBearer.startsWith('sk_trial_');
|
|
962
|
+
if (isClerkJwt) {
|
|
963
|
+
// Attempt Clerk JWT → Fodda API key resolution
|
|
964
|
+
try {
|
|
965
|
+
const resolveUrl = `${API_BASE_URL}/v1/auth/clerk-resolve`;
|
|
966
|
+
const resolveResp = await axios.post(resolveUrl, { clerk_jwt: rawBearer }, {
|
|
967
|
+
headers: { 'X-Internal-Key': process.env.FODDA_INTERNAL_API_KEY || '' },
|
|
968
|
+
timeout: 5000,
|
|
969
|
+
});
|
|
970
|
+
if (resolveResp.data?.api_key) {
|
|
971
|
+
req.__resolvedApiKey = resolveResp.data.api_key;
|
|
972
|
+
req.__resolvedUserId = resolveResp.data.user_id || 'oauth_user';
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
return res.status(401).json({
|
|
976
|
+
jsonrpc: '2.0',
|
|
977
|
+
error: { code: -32000, message: 'OAuth token could not be resolved to a Fodda account. Visit https://app.fodda.ai to reconnect.' },
|
|
978
|
+
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
catch (clerkErr) {
|
|
983
|
+
const status = clerkErr.response?.status;
|
|
984
|
+
if (status === 404 || status === 501) {
|
|
985
|
+
return res.status(501).json({
|
|
986
|
+
jsonrpc: '2.0',
|
|
987
|
+
error: { code: -32000, message: 'OAuth token resolution is not yet available on this server. Please use header API key auth (Authorization: Bearer sk_live_...) from https://app.fodda.ai.' },
|
|
988
|
+
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
return res.status(401).json({
|
|
992
|
+
jsonrpc: '2.0',
|
|
993
|
+
error: { code: -32000, message: 'OAuth token validation failed. Please reconnect at https://app.fodda.ai.' },
|
|
994
|
+
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const token = req.params.token;
|
|
999
|
+
let resolvedApiKey = '';
|
|
1000
|
+
let resolvedEmail = '';
|
|
1001
|
+
let resolvedEntryId = '';
|
|
1002
|
+
if (token) {
|
|
1003
|
+
try {
|
|
1004
|
+
const resolved = await resolveMcpToken(token, WEBSITE_BASE_URL);
|
|
1005
|
+
resolvedApiKey = resolved.apiKey;
|
|
1006
|
+
resolvedEmail = resolved.email;
|
|
1007
|
+
resolvedEntryId = resolved.analystId;
|
|
1008
|
+
}
|
|
1009
|
+
catch (err) {
|
|
1010
|
+
console.error(`[token-resolver] Failed to resolve connection token:`, err.message);
|
|
1011
|
+
return res.status(401).json({ error: 'Invalid or expired connection token' });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Extract API key, userId, and entry ID from URL or headers.
|
|
1015
|
+
// Priority: resolved OAuth key → X-API-Key header → Authorization Bearer (if non-Clerk sk_live_/sk_trial_) → resolved token key → query string (fallback).
|
|
1016
|
+
const resolvedOAuthKey = req.__resolvedApiKey || '';
|
|
1017
|
+
const resolvedOAuthUser = req.__resolvedUserId || '';
|
|
1018
|
+
const apiKey = isSpt ? '' : (resolvedOAuthKey
|
|
539
1019
|
|| req.headers['x-api-key']
|
|
540
|
-
|| (req.headers['authorization']?.toString().replace(/^Bearer\s+/i, ''))
|
|
1020
|
+
|| (!isClerkJwt ? (req.headers['authorization']?.toString().replace(/^Bearer\s+/i, '')) : '')
|
|
1021
|
+
|| resolvedApiKey
|
|
1022
|
+
|| req.query.api_key
|
|
541
1023
|
|| '');
|
|
542
|
-
const entryId = req.query.id || '';
|
|
1024
|
+
const entryId = resolvedEntryId || req.query.id || '';
|
|
543
1025
|
// If id looks like an email and no explicit user_id, use it as userId for tracking + signup
|
|
544
1026
|
const isEmailId = entryId.includes('@') && entryId.includes('.');
|
|
545
|
-
|
|
1027
|
+
// Priority for userId: resolved OAuth user → resolved token email → query user_id → header X-User-Id → query id / anonymous
|
|
1028
|
+
const userId = isSpt ? 'spt_agent' : (resolvedOAuthUser
|
|
1029
|
+
|| resolvedEmail
|
|
1030
|
+
|| req.query.user_id
|
|
546
1031
|
|| req.headers['x-user-id']
|
|
547
1032
|
|| (isEmailId ? entryId : 'anonymous'));
|
|
548
|
-
const
|
|
1033
|
+
const defaultSource = (offeringSlug !== 'mcp' && allowedTools !== undefined) ? offeringSlug : (isSpt ? 'spt' : '');
|
|
1034
|
+
const source = req.headers['x-fodda-source'] || req.query.source || defaultSource;
|
|
549
1035
|
if (sessionId && transports.has(sessionId)) {
|
|
550
1036
|
transport = transports.get(sessionId);
|
|
551
1037
|
}
|
|
552
1038
|
else if (!sessionId && req.method === 'POST') {
|
|
553
1039
|
const body = req.body;
|
|
554
1040
|
if (body?.method === 'initialize') {
|
|
1041
|
+
// Directory connector policy: /mcp requires authentication — Clerk OAuth,
|
|
1042
|
+
// an API key, or a /c/<token> connection URL. An anonymous handshake gets
|
|
1043
|
+
// 401 + WWW-Authenticate (RFC 9728) so MCP clients auto-start the OAuth
|
|
1044
|
+
// flow. Set MCP_ALLOW_ANONYMOUS=true to re-open the anonymous trial lane.
|
|
1045
|
+
if ((offeringSlug === 'mcp' || offeringSlug === 'copilot') && !isSpt && !apiKey && process.env.MCP_ALLOW_ANONYMOUS !== 'true') {
|
|
1046
|
+
res.setHeader('WWW-Authenticate', 'Bearer resource_metadata="https://mcp.fodda.ai/.well-known/oauth-protected-resource/mcp"');
|
|
1047
|
+
return res.status(401).json({
|
|
1048
|
+
jsonrpc: '2.0',
|
|
1049
|
+
error: { code: -32000, message: 'Authentication required. Connect via OAuth, or use your personal connection URL or API key from https://app.fodda.ai.' },
|
|
1050
|
+
id: body?.id ?? null,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
555
1053
|
// For an SPT (anonymous) session: validate the token ONCE now (no charge)
|
|
556
1054
|
// so we can refuse a task the SPT can't cover before running it. Reject the
|
|
557
1055
|
// connection if the token is bad/unfunded.
|
|
1056
|
+
//
|
|
1057
|
+
// Directory connector policy: SPT lane is disabled on this endpoint when
|
|
1058
|
+
// ENABLE_SPT !== 'true'. The listed connector is OAuth + account credits only.
|
|
558
1059
|
let sptInfo = null;
|
|
559
1060
|
if (isSpt) {
|
|
1061
|
+
if (process.env.ENABLE_SPT !== 'true') {
|
|
1062
|
+
return res.status(401).json({
|
|
1063
|
+
jsonrpc: '2.0',
|
|
1064
|
+
error: { code: -32000, message: 'SPT payment tokens are not accepted on this endpoint. Connect via OAuth at https://app.fodda.ai.' },
|
|
1065
|
+
id: body?.id ?? null,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
560
1068
|
const v = await validateSpt(spt);
|
|
561
1069
|
if (!v.valid) {
|
|
562
1070
|
return res.status(402).json({
|
|
@@ -576,7 +1084,7 @@ app.all('/mcp', async (req, res) => {
|
|
|
576
1084
|
: (source
|
|
577
1085
|
? ((m, p, k, u, b, r) => foddaRequest(m, p, k, u, b, r, source))
|
|
578
1086
|
: foddaRequest);
|
|
579
|
-
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl, entryId, sptInfo ?? undefined);
|
|
1087
|
+
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl, entryId, sptInfo ?? undefined, allowedTools);
|
|
580
1088
|
transport = new StreamableHTTPServerTransport({
|
|
581
1089
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
582
1090
|
onsessioninitialized: (sid) => {
|
|
@@ -685,7 +1193,9 @@ app.get('/health', (_req, res) => res.json({ status: 'ok', version: MCP_SERVER_V
|
|
|
685
1193
|
// ---------------------------------------------------------------------------
|
|
686
1194
|
// A2A Protocol Endpoint — agent-to-agent task delegation
|
|
687
1195
|
// ---------------------------------------------------------------------------
|
|
688
|
-
registerA2ARoute(app, foddaRequest);
|
|
1196
|
+
registerA2ARoute(app, foddaRequest, waverunnerRequest);
|
|
1197
|
+
// AgentFacts (NANDA) identity document — projection of the A2A card + version
|
|
1198
|
+
registerAgentFactsRoute(app, getServiceUrl);
|
|
689
1199
|
const PORT = parseInt(process.env.PORT || '8080');
|
|
690
1200
|
// Initialize catalog and pricing caches before accepting connections
|
|
691
1201
|
Promise.all([
|