fodda-mcp 1.46.31 → 1.46.97
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 -94
- 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 +1 -1
- package/dist/brandTemplate.d.ts +1 -1
- package/dist/brandTemplate.d.ts.map +1 -1
- package/dist/brandTemplate.js +162 -74
- package/dist/brandTemplate.js.map +1 -1
- package/dist/catalogCache.d.ts +29 -1
- package/dist/catalogCache.d.ts.map +1 -1
- package/dist/catalogCache.js +267 -16
- package/dist/catalogCache.js.map +1 -1
- package/dist/coverageRelevance.d.ts +97 -2
- package/dist/coverageRelevance.d.ts.map +1 -1
- package/dist/coverageRelevance.js +959 -164
- package/dist/coverageRelevance.js.map +1 -1
- package/dist/editorialFill.d.ts +5 -0
- package/dist/editorialFill.d.ts.map +1 -1
- package/dist/editorialFill.js +1 -1
- package/dist/editorialFill.js.map +1 -1
- package/dist/enrichment.d.ts +26 -0
- package/dist/enrichment.d.ts.map +1 -1
- package/dist/enrichment.js +394 -31
- package/dist/enrichment.js.map +1 -1
- package/dist/errorHandling.d.ts +35 -7
- package/dist/errorHandling.d.ts.map +1 -1
- package/dist/errorHandling.js +225 -155
- package/dist/errorHandling.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +351 -143
- package/dist/index.js.map +1 -1
- package/dist/jobs/jlens_quarterly_sweep.d.ts +62 -0
- package/dist/jobs/jlens_quarterly_sweep.d.ts.map +1 -0
- package/dist/jobs/jlens_quarterly_sweep.js +160 -0
- package/dist/jobs/jlens_quarterly_sweep.js.map +1 -0
- package/dist/pricingCache.d.ts.map +1 -1
- package/dist/pricingCache.js +41 -1
- package/dist/pricingCache.js.map +1 -1
- package/dist/queryCache.d.ts.map +1 -1
- package/dist/queryCache.js +2 -0
- package/dist/queryCache.js.map +1 -1
- package/dist/reportBriefing.d.ts +89 -0
- package/dist/reportBriefing.d.ts.map +1 -0
- package/dist/reportBriefing.js +662 -0
- package/dist/reportBriefing.js.map +1 -0
- package/dist/searchTemplate.d.ts.map +1 -1
- package/dist/searchTemplate.js +65 -53
- package/dist/searchTemplate.js.map +1 -1
- package/dist/services/graphRouter.d.ts +7 -0
- package/dist/services/graphRouter.d.ts.map +1 -0
- package/dist/services/graphRouter.js +7 -0
- package/dist/services/graphRouter.js.map +1 -0
- package/dist/services/notifications.d.ts +49 -0
- package/dist/services/notifications.d.ts.map +1 -0
- package/dist/services/notifications.js +206 -0
- package/dist/services/notifications.js.map +1 -0
- package/dist/sessionTracker.d.ts +19 -2
- package/dist/sessionTracker.d.ts.map +1 -1
- package/dist/sessionTracker.js +69 -16
- package/dist/sessionTracker.js.map +1 -1
- package/dist/skillClient.d.ts.map +1 -1
- package/dist/skillClient.js +11 -6
- package/dist/skillClient.js.map +1 -1
- package/dist/svgVisuals.d.ts +17 -19
- package/dist/svgVisuals.d.ts.map +1 -1
- package/dist/svgVisuals.js +189 -233
- package/dist/svgVisuals.js.map +1 -1
- package/dist/systemPrompt.d.ts +7 -2
- package/dist/systemPrompt.d.ts.map +1 -1
- package/dist/systemPrompt.js +110 -68
- package/dist/systemPrompt.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +15 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/test_adjacent_trends_polymorphic.d.ts +2 -0
- package/dist/test_adjacent_trends_polymorphic.d.ts.map +1 -0
- package/dist/test_adjacent_trends_polymorphic.js +154 -0
- package/dist/test_adjacent_trends_polymorphic.js.map +1 -0
- package/dist/test_book_a_call_live.js +18 -5
- package/dist/test_book_a_call_live.js.map +1 -1
- package/dist/test_byo_mcp_submission_clarity.d.ts +2 -0
- package/dist/test_byo_mcp_submission_clarity.d.ts.map +1 -0
- package/dist/test_byo_mcp_submission_clarity.js +142 -0
- package/dist/test_byo_mcp_submission_clarity.js.map +1 -0
- package/dist/test_capabilities_guidance.d.ts +2 -0
- package/dist/test_capabilities_guidance.d.ts.map +1 -0
- package/dist/test_capabilities_guidance.js +372 -0
- package/dist/test_capabilities_guidance.js.map +1 -0
- package/dist/test_client_provenance.d.ts +2 -0
- package/dist/test_client_provenance.d.ts.map +1 -0
- package/dist/test_client_provenance.js +77 -0
- package/dist/test_client_provenance.js.map +1 -0
- package/dist/test_connection_surface_cleanup.d.ts +2 -0
- package/dist/test_connection_surface_cleanup.d.ts.map +1 -0
- package/dist/test_connection_surface_cleanup.js +203 -0
- package/dist/test_connection_surface_cleanup.js.map +1 -0
- package/dist/test_consult_routing.d.ts +2 -0
- package/dist/test_consult_routing.d.ts.map +1 -0
- package/dist/test_consult_routing.js +86 -0
- package/dist/test_consult_routing.js.map +1 -0
- package/dist/test_consult_split.d.ts +2 -0
- package/dist/test_consult_split.d.ts.map +1 -0
- package/dist/test_consult_split.js +30 -0
- package/dist/test_consult_split.js.map +1 -0
- package/dist/test_coverage_relevance.js +111 -70
- package/dist/test_coverage_relevance.js.map +1 -1
- package/dist/test_coverage_replay.d.ts +2 -0
- package/dist/test_coverage_replay.d.ts.map +1 -0
- package/dist/test_coverage_replay.js +170 -0
- package/dist/test_coverage_replay.js.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.js +45 -50
- package/dist/test_dcr_and_legacy_deprecation.js.map +1 -1
- package/dist/test_demand_signal_logging.d.ts +2 -0
- package/dist/test_demand_signal_logging.d.ts.map +1 -0
- package/dist/test_demand_signal_logging.js +92 -0
- package/dist/test_demand_signal_logging.js.map +1 -0
- package/dist/test_earnings_activity_scope.d.ts +5 -0
- package/dist/test_earnings_activity_scope.d.ts.map +1 -0
- package/dist/test_earnings_activity_scope.js +101 -0
- package/dist/test_earnings_activity_scope.js.map +1 -0
- package/dist/test_earnings_divergence_live.d.ts +5 -0
- package/dist/test_earnings_divergence_live.d.ts.map +1 -0
- package/dist/test_earnings_divergence_live.js +135 -0
- package/dist/test_earnings_divergence_live.js.map +1 -0
- package/dist/test_earnings_hygiene_and_binding.d.ts +5 -0
- package/dist/test_earnings_hygiene_and_binding.d.ts.map +1 -0
- package/dist/test_earnings_hygiene_and_binding.js +104 -0
- package/dist/test_earnings_hygiene_and_binding.js.map +1 -0
- package/dist/test_exec_quotes_render.d.ts +2 -0
- package/dist/test_exec_quotes_render.d.ts.map +1 -0
- package/dist/test_exec_quotes_render.js +86 -0
- package/dist/test_exec_quotes_render.js.map +1 -0
- package/dist/test_expert_intro.d.ts +2 -0
- package/dist/test_expert_intro.d.ts.map +1 -0
- package/dist/test_expert_intro.js +57 -0
- package/dist/test_expert_intro.js.map +1 -0
- package/dist/test_expert_layer.d.ts +11 -0
- package/dist/test_expert_layer.d.ts.map +1 -0
- package/dist/test_expert_layer.js +356 -0
- package/dist/test_expert_layer.js.map +1 -0
- package/dist/test_gap_alert.js +8 -0
- package/dist/test_gap_alert.js.map +1 -1
- package/dist/test_grok_offering_surfaces.d.ts +2 -0
- package/dist/test_grok_offering_surfaces.d.ts.map +1 -0
- package/dist/test_grok_offering_surfaces.js +155 -0
- package/dist/test_grok_offering_surfaces.js.map +1 -0
- package/dist/test_human_agent_live.d.ts +2 -0
- package/dist/test_human_agent_live.d.ts.map +1 -0
- package/dist/test_human_agent_live.js +48 -0
- package/dist/test_human_agent_live.js.map +1 -0
- package/dist/test_identity_gap.d.ts +2 -0
- package/dist/test_identity_gap.d.ts.map +1 -0
- package/dist/test_identity_gap.js +171 -0
- package/dist/test_identity_gap.js.map +1 -0
- package/dist/test_intelligence_and_verifier.d.ts +17 -0
- package/dist/test_intelligence_and_verifier.d.ts.map +1 -0
- package/dist/test_intelligence_and_verifier.js +162 -0
- package/dist/test_intelligence_and_verifier.js.map +1 -0
- package/dist/test_latency_mcp_brief5.d.ts +12 -0
- package/dist/test_latency_mcp_brief5.d.ts.map +1 -0
- package/dist/test_latency_mcp_brief5.js +200 -0
- package/dist/test_latency_mcp_brief5.js.map +1 -0
- package/dist/test_limit_and_credit_card_capture.d.ts +21 -0
- package/dist/test_limit_and_credit_card_capture.d.ts.map +1 -0
- package/dist/test_limit_and_credit_card_capture.js +216 -0
- package/dist/test_limit_and_credit_card_capture.js.map +1 -0
- package/dist/test_list_graphs_topics.js +1 -1
- package/dist/test_list_graphs_topics.js.map +1 -1
- package/dist/test_live_deployed_mcp_search.d.ts +2 -0
- package/dist/test_live_deployed_mcp_search.d.ts.map +1 -0
- package/dist/test_live_deployed_mcp_search.js +85 -0
- package/dist/test_live_deployed_mcp_search.js.map +1 -0
- package/dist/test_live_deployed_split.d.ts +2 -0
- package/dist/test_live_deployed_split.d.ts.map +1 -0
- package/dist/test_live_deployed_split.js +72 -0
- package/dist/test_live_deployed_split.js.map +1 -0
- package/dist/test_live_endpoint_runner.js +2 -2
- package/dist/test_live_endpoint_runner.js.map +1 -1
- package/dist/test_live_mcp.js +1 -1
- package/dist/test_live_mcp.js.map +1 -1
- package/dist/test_live_neil_widget.d.ts +2 -0
- package/dist/test_live_neil_widget.d.ts.map +1 -0
- package/dist/test_live_neil_widget.js +84 -0
- package/dist/test_live_neil_widget.js.map +1 -0
- package/dist/test_longpoll_status.d.ts +2 -0
- package/dist/test_longpoll_status.d.ts.map +1 -0
- package/dist/test_longpoll_status.js +117 -0
- package/dist/test_longpoll_status.js.map +1 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts +2 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts.map +1 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.js +130 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.js.map +1 -0
- package/dist/test_next_moves.js +644 -279
- package/dist/test_next_moves.js.map +1 -1
- package/dist/test_next_moves_transcripts.js +122 -40
- package/dist/test_next_moves_transcripts.js.map +1 -1
- package/dist/test_on_request_caller_identity.d.ts +13 -0
- package/dist/test_on_request_caller_identity.d.ts.map +1 -0
- package/dist/test_on_request_caller_identity.js +131 -0
- package/dist/test_on_request_caller_identity.js.map +1 -0
- package/dist/test_on_request_intercept.d.ts +10 -0
- package/dist/test_on_request_intercept.d.ts.map +1 -0
- package/dist/test_on_request_intercept.js +95 -0
- package/dist/test_on_request_intercept.js.map +1 -0
- package/dist/test_onboarding_consent_and_errors.d.ts +2 -0
- package/dist/test_onboarding_consent_and_errors.d.ts.map +1 -0
- package/dist/test_onboarding_consent_and_errors.js +110 -0
- package/dist/test_onboarding_consent_and_errors.js.map +1 -0
- package/dist/test_onboarding_live_scenarios.d.ts +2 -0
- package/dist/test_onboarding_live_scenarios.d.ts.map +1 -0
- package/dist/test_onboarding_live_scenarios.js +189 -0
- package/dist/test_onboarding_live_scenarios.js.map +1 -0
- package/dist/test_referral_live.d.ts +2 -0
- package/dist/test_referral_live.d.ts.map +1 -0
- package/dist/test_referral_live.js +33 -0
- package/dist/test_referral_live.js.map +1 -0
- package/dist/test_referral_piers.d.ts +2 -0
- package/dist/test_referral_piers.d.ts.map +1 -0
- package/dist/test_referral_piers.js +33 -0
- package/dist/test_referral_piers.js.map +1 -0
- package/dist/test_report_editorial_briefing.d.ts +21 -0
- package/dist/test_report_editorial_briefing.d.ts.map +1 -0
- package/dist/test_report_editorial_briefing.js +356 -0
- package/dist/test_report_editorial_briefing.js.map +1 -0
- package/dist/test_search_graph_quality.d.ts +2 -0
- package/dist/test_search_graph_quality.d.ts.map +1 -0
- package/dist/test_search_graph_quality.js +493 -0
- package/dist/test_search_graph_quality.js.map +1 -0
- package/dist/test_service_to_service_auth.d.ts +2 -0
- package/dist/test_service_to_service_auth.d.ts.map +1 -0
- package/dist/test_service_to_service_auth.js +199 -0
- package/dist/test_service_to_service_auth.js.map +1 -0
- package/dist/test_sources_live.d.ts +2 -0
- package/dist/test_sources_live.d.ts.map +1 -0
- package/dist/test_sources_live.js +30 -0
- package/dist/test_sources_live.js.map +1 -0
- package/dist/test_specialist_and_analyst_tiers.d.ts +2 -0
- package/dist/test_specialist_and_analyst_tiers.d.ts.map +1 -0
- package/dist/test_specialist_and_analyst_tiers.js +155 -0
- package/dist/test_specialist_and_analyst_tiers.js.map +1 -0
- package/dist/test_v2_2_visual_hardening.d.ts +5 -0
- package/dist/test_v2_2_visual_hardening.d.ts.map +1 -0
- package/dist/test_v2_2_visual_hardening.js +333 -0
- package/dist/test_v2_2_visual_hardening.js.map +1 -0
- package/dist/test_verify_deprecate_render_spec.d.ts +2 -0
- package/dist/test_verify_deprecate_render_spec.d.ts.map +1 -0
- package/dist/test_verify_deprecate_render_spec.js +217 -0
- package/dist/test_verify_deprecate_render_spec.js.map +1 -0
- package/dist/test_verify_neil_demo.d.ts +2 -0
- package/dist/test_verify_neil_demo.d.ts.map +1 -0
- package/dist/test_verify_neil_demo.js +149 -0
- package/dist/test_verify_neil_demo.js.map +1 -0
- package/dist/test_waitable_research_and_grounded_links.d.ts +2 -0
- package/dist/test_waitable_research_and_grounded_links.d.ts.map +1 -0
- package/dist/test_waitable_research_and_grounded_links.js +129 -0
- package/dist/test_waitable_research_and_grounded_links.js.map +1 -0
- package/dist/test_widget_guards.d.ts +2 -0
- package/dist/test_widget_guards.d.ts.map +1 -0
- package/dist/test_widget_guards.js +229 -0
- package/dist/test_widget_guards.js.map +1 -0
- package/dist/test_zero_retention_alerts.d.ts +14 -0
- package/dist/test_zero_retention_alerts.d.ts.map +1 -0
- package/dist/test_zero_retention_alerts.js +76 -0
- package/dist/test_zero_retention_alerts.js.map +1 -0
- package/dist/toolHandlers.d.ts +38 -1
- package/dist/toolHandlers.d.ts.map +1 -1
- package/dist/toolHandlers.js +3856 -938
- package/dist/toolHandlers.js.map +1 -1
- package/dist/tools.d.ts +20 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +25 -4
- package/dist/tools.js.map +1 -1
- package/dist/verify_discoverability.d.ts +2 -0
- package/dist/verify_discoverability.d.ts.map +1 -0
- package/dist/verify_discoverability.js +53 -0
- package/dist/verify_discoverability.js.map +1 -0
- package/dist/verify_tools_endpoint.js +16 -3
- package/dist/verify_tools_endpoint.js.map +1 -1
- package/dist/widgetShell.d.ts +2 -2
- package/dist/widgetShell.d.ts.map +1 -1
- package/dist/widgetShell.js +1 -1
- package/dist/widgetShell.js.map +1 -1
- package/package.json +1 -1
- package/server.json +4 -32
- package/dist/oauthRegisterShim.d.ts +0 -13
- package/dist/oauthRegisterShim.d.ts.map +0 -1
- package/dist/oauthRegisterShim.js +0 -83
- package/dist/oauthRegisterShim.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import express from 'express';
|
|
8
8
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
9
|
-
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
10
9
|
import crypto from 'crypto';
|
|
11
10
|
import axios from 'axios';
|
|
12
11
|
import { initCatalogCache } from './catalogCache.js';
|
|
@@ -17,7 +16,6 @@ import { createServer } from './toolHandlers.js';
|
|
|
17
16
|
import { registerA2ARoute } from './a2aHandler.js';
|
|
18
17
|
import { registerAgentFactsRoute } from './agentFacts.js';
|
|
19
18
|
import { getTelemetryStats, recordFeedbackEntry } from './telemetry.js';
|
|
20
|
-
import { handleOauthRegister } from './oauthRegisterShim.js';
|
|
21
19
|
import * as dotenv from 'dotenv';
|
|
22
20
|
dotenv.config();
|
|
23
21
|
// ── Global crash guards ──
|
|
@@ -33,7 +31,7 @@ process.on('uncaughtException', (err) => {
|
|
|
33
31
|
});
|
|
34
32
|
const API_BASE_URL = process.env.FODDA_API_URL || 'https://api.fodda.ai';
|
|
35
33
|
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'https://www.fodda.ai';
|
|
36
|
-
const app = express();
|
|
34
|
+
export const app = express();
|
|
37
35
|
app.use(express.json({ limit: '512kb' }));
|
|
38
36
|
// Strip api_key from request logging globally before anything hits Cloud Run logs
|
|
39
37
|
app.use((req, _res, next) => {
|
|
@@ -46,24 +44,137 @@ app.use((req, _res, next) => {
|
|
|
46
44
|
app.use((req, res, next) => {
|
|
47
45
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
48
46
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
49
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-User-Id, X-Stripe-SPT, Mcp-Session-Id, Accept');
|
|
47
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-User-Id, X-User-Email, X-Internal-Key, X-Fodda-Signature, X-Fodda-Timestamp, X-Stripe-SPT, Mcp-Session-Id, Accept, X-Fodda-Session-Kind, X-Fodda-Source, X-Fodda-Client');
|
|
50
48
|
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
51
49
|
if (req.method === 'OPTIONS')
|
|
52
50
|
return res.status(204).end();
|
|
53
51
|
next();
|
|
54
52
|
});
|
|
53
|
+
// Helper to verify incoming HMAC signature from internal services
|
|
54
|
+
export function isIncomingHmacValid(req) {
|
|
55
|
+
const secret = process.env.FODDA_MCP_SECRET;
|
|
56
|
+
if (!secret)
|
|
57
|
+
return false;
|
|
58
|
+
const signature = req.headers['x-fodda-signature'] || '';
|
|
59
|
+
if (!signature)
|
|
60
|
+
return false;
|
|
61
|
+
const timestamp = req.headers['x-fodda-timestamp'] || '';
|
|
62
|
+
// If timestamp is supplied, enforce a 5-minute replay window (300,000 ms)
|
|
63
|
+
if (timestamp) {
|
|
64
|
+
const ts = parseInt(timestamp, 10);
|
|
65
|
+
if (isNaN(ts) || Math.abs(Date.now() - ts) > 300000) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Try payload with timestamp: `${timestamp}.${JSON.stringify(body)}` or `${timestamp}.${path}`
|
|
70
|
+
const payloadWithTs = (req.method === 'POST' || req.method === 'PATCH')
|
|
71
|
+
? (timestamp ? `${timestamp}.${JSON.stringify(req.body ?? {})}` : JSON.stringify(req.body ?? {}))
|
|
72
|
+
: (timestamp ? `${timestamp}.${req.originalUrl || req.url}` : (req.originalUrl || req.url));
|
|
73
|
+
const expectedWithTs = crypto.createHmac('sha256', secret).update(payloadWithTs).digest('hex');
|
|
74
|
+
if (signature.length === expectedWithTs.length) {
|
|
75
|
+
try {
|
|
76
|
+
if (crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedWithTs, 'hex'))) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* ignore */ }
|
|
81
|
+
}
|
|
82
|
+
// Also support payload without timestamp fallback
|
|
83
|
+
if (timestamp) {
|
|
84
|
+
const fallbackPayload = (req.method === 'POST' || req.method === 'PATCH')
|
|
85
|
+
? JSON.stringify(req.body ?? {})
|
|
86
|
+
: (req.originalUrl || req.url);
|
|
87
|
+
const fallbackExpected = crypto.createHmac('sha256', secret).update(fallbackPayload).digest('hex');
|
|
88
|
+
if (signature.length === fallbackExpected.length) {
|
|
89
|
+
try {
|
|
90
|
+
if (crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(fallbackExpected, 'hex'))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Service URL & RFC 9728 401 Helper
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
function getServiceUrl() {
|
|
103
|
+
if (process.env.FODDA_SERVICE_URL)
|
|
104
|
+
return process.env.FODDA_SERVICE_URL;
|
|
105
|
+
if (process.env.NODE_ENV === 'production' || process.env.K_SERVICE) {
|
|
106
|
+
return 'https://mcp.fodda.ai';
|
|
107
|
+
}
|
|
108
|
+
return `http://localhost:${process.env.PORT || 8080}`;
|
|
109
|
+
}
|
|
110
|
+
export function sendAuth401(res, options) {
|
|
111
|
+
const rawSlug = options.offeringSlug;
|
|
112
|
+
const metadataSlug = (!rawSlug || rawSlug === 'c') ? 'mcp' : rawSlug;
|
|
113
|
+
const resourceMetadata = `${getServiceUrl()}/.well-known/oauth-protected-resource/${metadataSlug}`;
|
|
114
|
+
if (options.invalidToken) {
|
|
115
|
+
res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadata}", error="invalid_token"`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${resourceMetadata}"`);
|
|
119
|
+
}
|
|
120
|
+
const errorPayload = {
|
|
121
|
+
code: -32000,
|
|
122
|
+
message: options.message,
|
|
123
|
+
};
|
|
124
|
+
if (options.invalidToken) {
|
|
125
|
+
errorPayload.error = 'invalid_token';
|
|
126
|
+
errorPayload.data = { error: 'invalid_token', ...(options.data || {}) };
|
|
127
|
+
}
|
|
128
|
+
else if (options.data) {
|
|
129
|
+
errorPayload.data = options.data;
|
|
130
|
+
}
|
|
131
|
+
return res.status(401).json({
|
|
132
|
+
jsonrpc: '2.0',
|
|
133
|
+
error: errorPayload,
|
|
134
|
+
id: options.id ?? null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Retired SSE transport (v1.46.85) — explicitly return 410 Gone
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
app.all(['/sse', '/messages'], (_req, res) => {
|
|
141
|
+
res.status(410).json({
|
|
142
|
+
error: 'sse_retired',
|
|
143
|
+
message: 'Use Streamable HTTP at https://mcp.fodda.ai/mcp',
|
|
144
|
+
source: 'fodda-mcp'
|
|
145
|
+
});
|
|
146
|
+
});
|
|
55
147
|
// Deprecation middleware for legacy URL parameters (?api_key=... / ?user_id=...)
|
|
56
148
|
const LEGACY_DEPRECATION_PATHS = [
|
|
57
|
-
'/
|
|
149
|
+
'/mcp', '/copilot', '/chatgpt',
|
|
58
150
|
'/brand-intelligence', '/topic-research', '/deep-research',
|
|
59
|
-
'/earnings-intelligence', '/expert-consult'
|
|
151
|
+
'/earnings-intelligence', '/expert-consult',
|
|
152
|
+
'/grok-brand-context', '/c/:token', '/c'
|
|
60
153
|
];
|
|
61
154
|
app.use(LEGACY_DEPRECATION_PATHS, (req, res, next) => {
|
|
62
155
|
if (req.query.api_key !== undefined || req.query.user_id !== undefined) {
|
|
63
|
-
const
|
|
156
|
+
const authHeader = (req.headers['authorization'] || '').toString().trim();
|
|
157
|
+
const rawBearer = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
158
|
+
const internalKey = process.env.FODDA_INTERNAL_API_KEY || '';
|
|
159
|
+
const internalKeyHeader = req.headers['x-internal-key'] || '';
|
|
160
|
+
const apiKeyHeader = req.headers['x-api-key'] || '';
|
|
161
|
+
// Check if request carries modern credentials or internal service authorization
|
|
162
|
+
const hasBearerAuth = rawBearer.length > 0;
|
|
163
|
+
const hasInternalKey = !!(internalKey && (internalKeyHeader === internalKey ||
|
|
164
|
+
rawBearer === internalKey ||
|
|
165
|
+
apiKeyHeader === internalKey ||
|
|
166
|
+
req.query.api_key === internalKey));
|
|
167
|
+
const hasValidHmac = isIncomingHmacValid(req);
|
|
168
|
+
const hasHeaderApiKey = apiKeyHeader.startsWith('sk_live_') || apiKeyHeader.startsWith('sk_trial_');
|
|
169
|
+
// Service-to-service calls or requests supplying valid header credentials bypass deprecation
|
|
170
|
+
if (hasBearerAuth || hasInternalKey || hasValidHmac || hasHeaderApiKey) {
|
|
171
|
+
return next();
|
|
172
|
+
}
|
|
173
|
+
const message = 'Fodda: this connection URL is outdated. Get your new MCP URL at https://app.fodda.ai (Connections) and update your connector.';
|
|
64
174
|
const accept = req.headers['accept'] || '';
|
|
65
175
|
const contentType = req.headers['content-type'] || '';
|
|
66
176
|
const isJson = accept.includes('application/json') || contentType.includes('application/json') || (req.method === 'POST' && req.body && typeof req.body === 'object');
|
|
177
|
+
res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${getServiceUrl()}/.well-known/oauth-protected-resource/mcp"`);
|
|
67
178
|
if (isJson && req.method === 'POST') {
|
|
68
179
|
const bodyId = (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null;
|
|
69
180
|
return res.status(401).json({
|
|
@@ -71,7 +182,7 @@ app.use(LEGACY_DEPRECATION_PATHS, (req, res, next) => {
|
|
|
71
182
|
error: {
|
|
72
183
|
code: -32001,
|
|
73
184
|
message,
|
|
74
|
-
data: { docs: 'https://fodda.ai/
|
|
185
|
+
data: { docs: 'https://www.fodda.ai/connect' }
|
|
75
186
|
},
|
|
76
187
|
id: bodyId,
|
|
77
188
|
});
|
|
@@ -82,62 +193,22 @@ app.use(LEGACY_DEPRECATION_PATHS, (req, res, next) => {
|
|
|
82
193
|
next();
|
|
83
194
|
});
|
|
84
195
|
// OAuth discovery endpoints — Clerk-backed OAuth for the MCP Connectors Directory.
|
|
85
|
-
//
|
|
86
|
-
//
|
|
196
|
+
// Advertises Clerk (CLERK_ISSUER) as the authorization server for RFC 9728 discovery,
|
|
197
|
+
// ensuring RFC 8414 issuer matching and RFC 9207 callback iss validation succeed end-to-end.
|
|
87
198
|
const CLERK_ISSUER = process.env.CLERK_ISSUER_URL || 'https://clerk.fodda.ai';
|
|
88
|
-
app.get('/.well-known/oauth-protected-resource', (
|
|
199
|
+
app.get(['/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/:slug'], (req, res) => {
|
|
200
|
+
const slug = req.params.slug;
|
|
201
|
+
const resourceUrl = slug ? `${getServiceUrl()}/${slug}` : getServiceUrl();
|
|
89
202
|
res.status(200).json({
|
|
90
|
-
resource:
|
|
91
|
-
authorization_servers: [
|
|
203
|
+
resource: resourceUrl,
|
|
204
|
+
authorization_servers: [CLERK_ISSUER],
|
|
92
205
|
});
|
|
93
206
|
});
|
|
94
|
-
app.get('/.well-known/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
207
|
+
app.get('/.well-known/openai-apps-challenge', (_req, res) => {
|
|
208
|
+
const challenge = process.env.OPENAI_APPS_CHALLENGE || '';
|
|
209
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
210
|
+
res.status(200).send(challenge);
|
|
99
211
|
});
|
|
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);
|
|
139
|
-
});
|
|
140
|
-
app.post(['/oauth/register', '/register'], handleOauthRegister);
|
|
141
212
|
// ---------------------------------------------------------------------------
|
|
142
213
|
// .well-known MCP Server Discovery Card & Pricing Metadata Tier
|
|
143
214
|
// ---------------------------------------------------------------------------
|
|
@@ -207,8 +278,11 @@ export const OFFERING_SCOPED_TOOLS = {
|
|
|
207
278
|
],
|
|
208
279
|
'expert-consult': [
|
|
209
280
|
'get_capabilities',
|
|
281
|
+
'find_expert',
|
|
282
|
+
'request_expert_intro',
|
|
210
283
|
'consult_analyst',
|
|
211
284
|
'consult_human_agent',
|
|
285
|
+
'verify_claim',
|
|
212
286
|
'list_analysts',
|
|
213
287
|
'request_deliverable',
|
|
214
288
|
'check_deliverable_status',
|
|
@@ -231,15 +305,53 @@ export const OFFERING_SCOPED_TOOLS = {
|
|
|
231
305
|
'get_company_earnings',
|
|
232
306
|
'deep_research_topic',
|
|
233
307
|
'check_research_status',
|
|
308
|
+
'find_expert',
|
|
309
|
+
'request_expert_intro',
|
|
310
|
+
'consult_analyst',
|
|
311
|
+
'consult_human_agent',
|
|
312
|
+
'list_analysts',
|
|
313
|
+
'get_evidence',
|
|
314
|
+
'list_graphs',
|
|
315
|
+
'get_my_account',
|
|
316
|
+
'read_url',
|
|
317
|
+
'get_supplemental_context',
|
|
318
|
+
'check_supplemental_status',
|
|
319
|
+
],
|
|
320
|
+
'chatgpt': [
|
|
321
|
+
'get_capabilities',
|
|
322
|
+
'search_graph',
|
|
323
|
+
'search_statistics',
|
|
324
|
+
'search_insights',
|
|
325
|
+
'get_validated_trends',
|
|
326
|
+
'brand_tracker',
|
|
327
|
+
'get_company_earnings',
|
|
328
|
+
'get_earnings_intelligence',
|
|
329
|
+
'get_earnings_divergence',
|
|
330
|
+
'deep_research_topic',
|
|
331
|
+
'check_research_status',
|
|
234
332
|
'consult_analyst',
|
|
235
333
|
'consult_human_agent',
|
|
236
334
|
'list_analysts',
|
|
237
335
|
'get_evidence',
|
|
336
|
+
'get_node',
|
|
337
|
+
'get_neighbors',
|
|
338
|
+
'get_label_values',
|
|
238
339
|
'list_graphs',
|
|
239
340
|
'get_my_account',
|
|
240
341
|
'read_url',
|
|
241
342
|
'get_supplemental_context',
|
|
242
343
|
'check_supplemental_status',
|
|
344
|
+
'generate_visual',
|
|
345
|
+
],
|
|
346
|
+
'grok-brand-context': [
|
|
347
|
+
'get_capabilities',
|
|
348
|
+
'get_domain_intelligence',
|
|
349
|
+
'brand_tracker',
|
|
350
|
+
'discover_adjacent_trends',
|
|
351
|
+
'get_earnings_intelligence',
|
|
352
|
+
'get_evidence',
|
|
353
|
+
'find_expert',
|
|
354
|
+
'verify_claim',
|
|
243
355
|
],
|
|
244
356
|
};
|
|
245
357
|
const OFFERING_CARD_METADATA = {
|
|
@@ -273,6 +385,16 @@ const OFFERING_CARD_METADATA = {
|
|
|
273
385
|
title: 'Fodda Copilot Market Intelligence',
|
|
274
386
|
description: 'Curated market intelligence, trend insights & expert evidence for Microsoft Copilot Studio agents.',
|
|
275
387
|
},
|
|
388
|
+
'chatgpt': {
|
|
389
|
+
name: 'ai.fodda/chatgpt',
|
|
390
|
+
title: 'Fodda Market Intelligence for ChatGPT',
|
|
391
|
+
description: 'Expert-curated knowledge, brand, research & earnings intelligence across PSFK expert graphs.',
|
|
392
|
+
},
|
|
393
|
+
'grok-brand-context': {
|
|
394
|
+
name: 'ai.fodda/grok-brand-context',
|
|
395
|
+
title: 'Fodda Brand & Market Context',
|
|
396
|
+
description: 'Verified brand, market & earnings context across PSFK expert graphs with citable evidence, not web summaries.',
|
|
397
|
+
},
|
|
276
398
|
};
|
|
277
399
|
app.get([
|
|
278
400
|
'/.well-known/mcp-server.json', '/.well-known/mcp',
|
|
@@ -282,6 +404,8 @@ app.get([
|
|
|
282
404
|
'/.well-known/earnings-intelligence', '/.well-known/earnings-intelligence.json',
|
|
283
405
|
'/.well-known/expert-consult', '/.well-known/expert-consult.json',
|
|
284
406
|
'/.well-known/copilot', '/.well-known/copilot.json',
|
|
407
|
+
'/.well-known/chatgpt', '/.well-known/chatgpt.json',
|
|
408
|
+
'/.well-known/grok-brand-context', '/.well-known/grok-brand-context.json',
|
|
285
409
|
], (req, res) => {
|
|
286
410
|
res.setHeader('Content-Type', 'application/json');
|
|
287
411
|
const matchedOffering = Object.keys(OFFERING_CARD_METADATA).find(slug => req.path.includes(slug));
|
|
@@ -302,7 +426,7 @@ app.get([
|
|
|
302
426
|
pricingTier: {
|
|
303
427
|
structure: 'free',
|
|
304
428
|
substance: 'metered',
|
|
305
|
-
tokenModel: '
|
|
429
|
+
tokenModel: 'Metered per API call — see https://fodda.ai/pricing',
|
|
306
430
|
},
|
|
307
431
|
resourceSchemes: [
|
|
308
432
|
'fodda://expert/{slug}/insight/{id}',
|
|
@@ -310,7 +434,6 @@ app.get([
|
|
|
310
434
|
],
|
|
311
435
|
endpoints: {
|
|
312
436
|
mcpStreamableHttp: `${getServiceUrl()}/${matchedOffering}`,
|
|
313
|
-
mcpSse: `${getServiceUrl()}/sse`,
|
|
314
437
|
telemetry: `${getServiceUrl()}/telemetry`,
|
|
315
438
|
feedback: `${getServiceUrl()}/v1/feedback`,
|
|
316
439
|
},
|
|
@@ -331,7 +454,7 @@ app.get([
|
|
|
331
454
|
pricingTier: {
|
|
332
455
|
structure: 'free',
|
|
333
456
|
substance: 'metered',
|
|
334
|
-
tokenModel: '
|
|
457
|
+
tokenModel: 'Metered per API call — see https://fodda.ai/pricing',
|
|
335
458
|
},
|
|
336
459
|
resourceSchemes: [
|
|
337
460
|
'fodda://expert/{slug}/insight/{id}',
|
|
@@ -339,7 +462,6 @@ app.get([
|
|
|
339
462
|
],
|
|
340
463
|
endpoints: {
|
|
341
464
|
mcpStreamableHttp: `${getServiceUrl()}/mcp`,
|
|
342
|
-
mcpSse: `${getServiceUrl()}/sse`,
|
|
343
465
|
telemetry: `${getServiceUrl()}/telemetry`,
|
|
344
466
|
feedback: `${getServiceUrl()}/v1/feedback`,
|
|
345
467
|
},
|
|
@@ -389,12 +511,56 @@ const transports = new Map();
|
|
|
389
511
|
const sessionApiKeys = new Map();
|
|
390
512
|
const sessionUserIds = new Map();
|
|
391
513
|
const sessionSources = new Map();
|
|
514
|
+
export const sessionClients = new Map();
|
|
392
515
|
// SPT-paying anonymous sessions (no API key). Holds the token + the connect-time
|
|
393
516
|
// validate result (cap + price map) so per-task coverage can be checked locally.
|
|
394
517
|
const sessionSpts = new Map();
|
|
395
518
|
// Track session creation time in our own Map (transport._createdAt is a private
|
|
396
519
|
// field that is never set by the SDK — reading it always yields undefined).
|
|
397
520
|
const sessionCreatedAt = new Map();
|
|
521
|
+
/**
|
|
522
|
+
* Normalize clientInfo from MCP initialize handshake to a short, canonical slug.
|
|
523
|
+
* E.g. Claude Desktop -> claude-desktop, Cursor -> cursor, VS Code -> vscode.
|
|
524
|
+
*/
|
|
525
|
+
export function normalizeClientSlug(name, version) {
|
|
526
|
+
if (!name || typeof name !== 'string')
|
|
527
|
+
return '';
|
|
528
|
+
const lower = name.toLowerCase().trim();
|
|
529
|
+
if (lower.includes('claude desktop') || lower.includes('claude-desktop') || lower.includes('claude for desktop')) {
|
|
530
|
+
return 'claude-desktop';
|
|
531
|
+
}
|
|
532
|
+
if (lower.includes('claude code') || lower.includes('claude-code')) {
|
|
533
|
+
return 'claude-code';
|
|
534
|
+
}
|
|
535
|
+
if (lower === 'claude' || lower.includes('claude.ai') || lower.startsWith('claude')) {
|
|
536
|
+
return 'claude';
|
|
537
|
+
}
|
|
538
|
+
if (lower.includes('cursor')) {
|
|
539
|
+
return 'cursor';
|
|
540
|
+
}
|
|
541
|
+
if (lower.includes('visual studio code') || lower.includes('vscode')) {
|
|
542
|
+
return 'vscode';
|
|
543
|
+
}
|
|
544
|
+
if (lower.includes('windsurf')) {
|
|
545
|
+
return 'windsurf';
|
|
546
|
+
}
|
|
547
|
+
if (lower.includes('chatgpt') || lower.includes('openai')) {
|
|
548
|
+
return 'chatgpt';
|
|
549
|
+
}
|
|
550
|
+
if (lower.includes('zed')) {
|
|
551
|
+
return 'zed';
|
|
552
|
+
}
|
|
553
|
+
if (lower.includes('librechat')) {
|
|
554
|
+
return 'librechat';
|
|
555
|
+
}
|
|
556
|
+
if (lower.includes('roo-code') || lower.includes('roo code') || lower.includes('roocode')) {
|
|
557
|
+
return 'roo-code';
|
|
558
|
+
}
|
|
559
|
+
if (lower.includes('cline')) {
|
|
560
|
+
return 'cline';
|
|
561
|
+
}
|
|
562
|
+
return lower.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
563
|
+
}
|
|
398
564
|
// ---------------------------------------------------------------------------
|
|
399
565
|
// Periodic cleanup — prevent unbounded memory growth
|
|
400
566
|
// ---------------------------------------------------------------------------
|
|
@@ -415,44 +581,40 @@ setInterval(() => {
|
|
|
415
581
|
sessionApiKeys.delete(sid);
|
|
416
582
|
sessionUserIds.delete(sid);
|
|
417
583
|
sessionSources.delete(sid);
|
|
584
|
+
sessionClients.delete(sid);
|
|
418
585
|
sessionSpts.delete(sid);
|
|
419
586
|
sessionCreatedAt.delete(sid);
|
|
420
587
|
}
|
|
421
588
|
}
|
|
422
589
|
}, CLEANUP_INTERVAL_MS).unref();
|
|
423
590
|
// ---------------------------------------------------------------------------
|
|
424
|
-
// Service URL helper
|
|
425
|
-
// ---------------------------------------------------------------------------
|
|
426
|
-
// M2: FODDA_SERVICE_URL must be set in Cloud Run env vars.
|
|
427
|
-
// Fallback to localhost for local dev only. The hardcoded project hash
|
|
428
|
-
// was removed — if it was being used, the URL was silently wrong.
|
|
429
|
-
function getServiceUrl() {
|
|
430
|
-
if (process.env.FODDA_SERVICE_URL)
|
|
431
|
-
return process.env.FODDA_SERVICE_URL;
|
|
432
|
-
if (process.env.NODE_ENV === 'production' || process.env.K_SERVICE) {
|
|
433
|
-
return 'https://mcp.fodda.ai';
|
|
434
|
-
}
|
|
435
|
-
return `http://localhost:${process.env.PORT || 8080}`;
|
|
436
|
-
}
|
|
437
|
-
// ---------------------------------------------------------------------------
|
|
438
591
|
// Authenticated API caller — checks query cache first
|
|
439
592
|
// ---------------------------------------------------------------------------
|
|
440
593
|
/**
|
|
441
594
|
* Make an authenticated request to the Fodda API.
|
|
442
595
|
* Checks the query cache first; stores responses on cache miss.
|
|
443
596
|
*/
|
|
444
|
-
|
|
597
|
+
const PLACEHOLDER_USER_IDS = new Set(['', 'anonymous', 'undefined', 'null', 'oauth_user']);
|
|
598
|
+
export function isPlaceholderUserId(id) {
|
|
599
|
+
if (!id)
|
|
600
|
+
return true;
|
|
601
|
+
return PLACEHOLDER_USER_IDS.has(id.trim().toLowerCase());
|
|
602
|
+
}
|
|
603
|
+
export async function foddaRequest(method, path, apiKey, userId, body, requestId, source, spt, client) {
|
|
445
604
|
// ── Cache check ──
|
|
446
605
|
const cached = cacheGet(method, path, body);
|
|
447
606
|
if (cached !== null)
|
|
448
607
|
return cached;
|
|
449
608
|
const timestamp = Date.now().toString();
|
|
450
609
|
const headers = {
|
|
451
|
-
'X-User-Id': userId,
|
|
452
610
|
'X-Fodda-Timestamp': timestamp,
|
|
453
611
|
'X-Fodda-Billing': 'mcp-orchestrated', // Tells API to skip per-call billing — MCP charges lump sum via meter
|
|
454
612
|
'Content-Type': 'application/json',
|
|
455
613
|
};
|
|
614
|
+
// Never send placeholder user IDs upstream — let the API's account-label fallback apply
|
|
615
|
+
if (userId && !isPlaceholderUserId(userId)) {
|
|
616
|
+
headers['X-User-Id'] = userId;
|
|
617
|
+
}
|
|
456
618
|
// SPT settlement: the Shared Payment Token is the payer (Authorization Bearer), no X-API-Key.
|
|
457
619
|
if (spt) {
|
|
458
620
|
headers['Authorization'] = `Bearer ${spt}`;
|
|
@@ -464,6 +626,8 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
464
626
|
headers['X-Request-Id'] = requestId;
|
|
465
627
|
if (source)
|
|
466
628
|
headers['X-Fodda-Source'] = source;
|
|
629
|
+
if (client)
|
|
630
|
+
headers['X-Fodda-Client'] = client;
|
|
467
631
|
// HMAC sign the request
|
|
468
632
|
const secret = process.env.FODDA_MCP_SECRET;
|
|
469
633
|
if (secret) {
|
|
@@ -473,11 +637,11 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
473
637
|
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
474
638
|
headers['X-Fodda-Signature'] = signature;
|
|
475
639
|
}
|
|
476
|
-
const baseUrl = path.startsWith('/api/') ? WEBSITE_BASE_URL : API_BASE_URL;
|
|
640
|
+
const baseUrl = path.startsWith('/api/') ? (process.env.WEBSITE_BASE_URL || WEBSITE_BASE_URL) : (process.env.FODDA_API_URL || API_BASE_URL);
|
|
477
641
|
const url = `${baseUrl}${path}`;
|
|
478
642
|
// Base timeout: 30s aligns with MCP client expectations.
|
|
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)
|
|
643
|
+
// Extended to 90s for analyst and human agent consults, verify/claim, and intelligence routes, and 35s for supplemental.
|
|
644
|
+
const AXIOS_TIMEOUT_MS = /(\/analysts\/consult|\/human-agents\/consult|\/verify\/|\/intelligence\/)/.test(path)
|
|
481
645
|
? 90000
|
|
482
646
|
: /\/supplemental\//.test(path)
|
|
483
647
|
? 35000
|
|
@@ -503,6 +667,12 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
503
667
|
const overageTokens = response.headers['x-usage-overage-tokens'];
|
|
504
668
|
if (overageTokens)
|
|
505
669
|
response.data._upstream_usage.overage_tokens = parseInt(overageTokens, 10);
|
|
670
|
+
const dailyRemaining = response.headers['x-usage-daily-remaining'];
|
|
671
|
+
if (dailyRemaining)
|
|
672
|
+
response.data._upstream_usage.daily_remaining = parseInt(dailyRemaining, 10);
|
|
673
|
+
const dailyCalls = response.headers['x-usage-daily-calls'];
|
|
674
|
+
if (dailyCalls)
|
|
675
|
+
response.data._upstream_usage.daily_calls = parseInt(dailyCalls, 10);
|
|
506
676
|
}
|
|
507
677
|
// ── Billing-mode trust check (Option C) ──
|
|
508
678
|
// Every MCP request claims mcp-orchestrated; the API echoes the EFFECTIVE mode.
|
|
@@ -939,7 +1109,7 @@ export async function resolveMcpToken(token, websiteBaseUrl) {
|
|
|
939
1109
|
// ---------------------------------------------------------------------------
|
|
940
1110
|
// MCP Transport Handler
|
|
941
1111
|
// ---------------------------------------------------------------------------
|
|
942
|
-
app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult', '/copilot', '/c/:token'], async (req, res) => {
|
|
1112
|
+
app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult', '/copilot', '/chatgpt', '/grok-brand-context', '/c/:token'], async (req, res) => {
|
|
943
1113
|
try {
|
|
944
1114
|
const sessionId = req.headers['mcp-session-id'];
|
|
945
1115
|
let transport;
|
|
@@ -956,9 +1126,17 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
956
1126
|
|| '';
|
|
957
1127
|
const isSpt = !!spt;
|
|
958
1128
|
const rawBearer = !isSpt ? rawAuth.replace(/^Bearer\s+/i, '').trim() : '';
|
|
1129
|
+
// Internal service auth detection
|
|
1130
|
+
const internalKey = process.env.FODDA_INTERNAL_API_KEY || '';
|
|
1131
|
+
const internalKeyHeader = req.headers['x-internal-key'] || '';
|
|
1132
|
+
const hasValidHmac = isIncomingHmacValid(req);
|
|
1133
|
+
const isInternalAuth = hasValidHmac || !!(internalKey && (internalKeyHeader === internalKey ||
|
|
1134
|
+
rawBearer === internalKey ||
|
|
1135
|
+
req.headers['x-api-key'] === internalKey ||
|
|
1136
|
+
req.query.api_key === internalKey));
|
|
959
1137
|
// 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_');
|
|
1138
|
+
// If it's not a standard sk_live_ / sk_trial_ key and not internal auth, send it to clerk-resolve.
|
|
1139
|
+
const isClerkJwt = !isSpt && !isInternalAuth && rawBearer.length > 0 && !rawBearer.startsWith('sk_live_') && !rawBearer.startsWith('sk_trial_');
|
|
962
1140
|
if (isClerkJwt) {
|
|
963
1141
|
// Attempt Clerk JWT → Fodda API key resolution
|
|
964
1142
|
try {
|
|
@@ -972,9 +1150,10 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
972
1150
|
req.__resolvedUserId = resolveResp.data.user_id || 'oauth_user';
|
|
973
1151
|
}
|
|
974
1152
|
else {
|
|
975
|
-
return res
|
|
976
|
-
|
|
977
|
-
|
|
1153
|
+
return sendAuth401(res, {
|
|
1154
|
+
offeringSlug,
|
|
1155
|
+
message: 'OAuth token could not be resolved to a Fodda account. Visit https://app.fodda.ai to reconnect.',
|
|
1156
|
+
invalidToken: true,
|
|
978
1157
|
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
979
1158
|
});
|
|
980
1159
|
}
|
|
@@ -988,9 +1167,10 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
988
1167
|
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
989
1168
|
});
|
|
990
1169
|
}
|
|
991
|
-
return res
|
|
992
|
-
|
|
993
|
-
|
|
1170
|
+
return sendAuth401(res, {
|
|
1171
|
+
offeringSlug,
|
|
1172
|
+
message: 'OAuth token validation failed. Please reconnect at https://app.fodda.ai.',
|
|
1173
|
+
invalidToken: true,
|
|
994
1174
|
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
995
1175
|
});
|
|
996
1176
|
}
|
|
@@ -1008,45 +1188,61 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1008
1188
|
}
|
|
1009
1189
|
catch (err) {
|
|
1010
1190
|
console.error(`[token-resolver] Failed to resolve connection token:`, err.message);
|
|
1011
|
-
return res
|
|
1191
|
+
return sendAuth401(res, {
|
|
1192
|
+
offeringSlug,
|
|
1193
|
+
message: 'Invalid or expired connection token. Visit https://app.fodda.ai to reconnect.',
|
|
1194
|
+
invalidToken: true,
|
|
1195
|
+
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
1196
|
+
});
|
|
1012
1197
|
}
|
|
1013
1198
|
}
|
|
1014
1199
|
// 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).
|
|
1200
|
+
// Priority: resolved OAuth key → X-API-Key header → Authorization Bearer (if non-Clerk sk_live_/sk_trial_) → internal service key → resolved token key → query string (fallback).
|
|
1016
1201
|
const resolvedOAuthKey = req.__resolvedApiKey || '';
|
|
1017
1202
|
const resolvedOAuthUser = req.__resolvedUserId || '';
|
|
1018
1203
|
const apiKey = isSpt ? '' : (resolvedOAuthKey
|
|
1019
1204
|
|| req.headers['x-api-key']
|
|
1020
|
-
|| (!isClerkJwt ?
|
|
1205
|
+
|| (!isClerkJwt ? rawBearer : '')
|
|
1206
|
+
|| (isInternalAuth ? (internalKey || 'sk_internal_service') : '')
|
|
1021
1207
|
|| resolvedApiKey
|
|
1022
1208
|
|| req.query.api_key
|
|
1023
1209
|
|| '');
|
|
1024
1210
|
const entryId = resolvedEntryId || req.query.id || '';
|
|
1025
1211
|
// If id looks like an email and no explicit user_id, use it as userId for tracking + signup
|
|
1026
1212
|
const isEmailId = entryId.includes('@') && entryId.includes('.');
|
|
1027
|
-
|
|
1213
|
+
const headerUserId = req.headers['x-user-id'] || req.headers['x-user-email'] || '';
|
|
1214
|
+
// Priority for userId: resolved OAuth user → resolved token email → header X-User-Id / X-User-Email → query user_id → query id / anonymous
|
|
1028
1215
|
const userId = isSpt ? 'spt_agent' : (resolvedOAuthUser
|
|
1029
1216
|
|| resolvedEmail
|
|
1217
|
+
|| headerUserId
|
|
1030
1218
|
|| req.query.user_id
|
|
1031
|
-
|| req.headers['x-user-id']
|
|
1032
1219
|
|| (isEmailId ? entryId : 'anonymous'));
|
|
1033
|
-
const
|
|
1034
|
-
const
|
|
1220
|
+
const defaultSessionKind = isInternalAuth ? 'internal-test' : 'customer';
|
|
1221
|
+
const sessionKind = req.headers['x-fodda-session-kind'] || req.query.session_kind || defaultSessionKind;
|
|
1222
|
+
const isInternalTest = sessionKind === 'internal-test';
|
|
1223
|
+
const defaultSource = isInternalTest ? 'mcp-internal-test' : ((offeringSlug !== 'mcp' && allowedTools !== undefined) ? offeringSlug : (isSpt ? 'spt' : ''));
|
|
1224
|
+
const rawSource = req.headers['x-fodda-source'] || req.query.source || '';
|
|
1225
|
+
const userAgent = (req.headers['user-agent'] || '').toString().toLowerCase();
|
|
1226
|
+
const isChatGptUa = userAgent.includes('chatgpt') || userAgent.includes('chatgpt-user') || userAgent.includes('openai');
|
|
1227
|
+
const source = isInternalTest ? 'mcp-internal-test' : (rawSource || (isChatGptUa ? 'chatgpt' : defaultSource));
|
|
1035
1228
|
if (sessionId && transports.has(sessionId)) {
|
|
1036
1229
|
transport = transports.get(sessionId);
|
|
1037
1230
|
}
|
|
1038
1231
|
else if (!sessionId && req.method === 'POST') {
|
|
1039
1232
|
const body = req.body;
|
|
1233
|
+
const clientInfo = body?.params?.clientInfo;
|
|
1234
|
+
const declaredClientSlug = normalizeClientSlug(clientInfo?.name, clientInfo?.version);
|
|
1235
|
+
const clientSlug = declaredClientSlug || (isChatGptUa ? 'chatgpt' : '');
|
|
1040
1236
|
if (body?.method === 'initialize') {
|
|
1041
|
-
// Directory connector policy:
|
|
1237
|
+
// Directory connector policy: All offering routes require authentication — Clerk OAuth,
|
|
1042
1238
|
// an API key, or a /c/<token> connection URL. An anonymous handshake gets
|
|
1043
1239
|
// 401 + WWW-Authenticate (RFC 9728) so MCP clients auto-start the OAuth
|
|
1044
1240
|
// flow. Set MCP_ALLOW_ANONYMOUS=true to re-open the anonymous trial lane.
|
|
1045
|
-
if (
|
|
1046
|
-
res
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1241
|
+
if (!isSpt && !apiKey && process.env.MCP_ALLOW_ANONYMOUS !== 'true') {
|
|
1242
|
+
return sendAuth401(res, {
|
|
1243
|
+
offeringSlug,
|
|
1244
|
+
message: 'Authentication required. Connect via OAuth, or use your personal connection URL or API key from https://app.fodda.ai.',
|
|
1245
|
+
invalidToken: false,
|
|
1050
1246
|
id: body?.id ?? null,
|
|
1051
1247
|
});
|
|
1052
1248
|
}
|
|
@@ -1078,23 +1274,22 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1078
1274
|
// Wrap foddaRequest. SPT sessions authenticate the internal fan-out with the
|
|
1079
1275
|
// internal service key (the SPT is spent ONCE at settlement, never on fan-out);
|
|
1080
1276
|
// otherwise bake in source attribution.
|
|
1081
|
-
const internalKey = process.env.FODDA_INTERNAL_API_KEY || '';
|
|
1082
1277
|
const boundFoddaRequest = isSpt
|
|
1083
|
-
? ((m, p, _k, u, b, r, _s, sptArg) => foddaRequest(m, p, sptArg ? '' : internalKey, u, b, r, 'spt', sptArg))
|
|
1084
|
-
: (source
|
|
1085
|
-
|
|
1086
|
-
: foddaRequest);
|
|
1087
|
-
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl, entryId, sptInfo ?? undefined, allowedTools);
|
|
1278
|
+
? ((m, p, _k, u, b, r, _s, sptArg) => foddaRequest(m, p, sptArg ? '' : internalKey, u, b, r, isInternalTest ? 'mcp-internal-test' : 'spt', sptArg, clientSlug || undefined))
|
|
1279
|
+
: ((m, p, k, u, b, r) => foddaRequest(m, p, k, u, b, r, source || undefined, undefined, clientSlug || undefined));
|
|
1280
|
+
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl, entryId, sptInfo ?? undefined, allowedTools, source || undefined);
|
|
1088
1281
|
transport = new StreamableHTTPServerTransport({
|
|
1089
1282
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
1090
1283
|
onsessioninitialized: (sid) => {
|
|
1091
|
-
console.error(`Session created: ${sid}`);
|
|
1284
|
+
console.error(`Session created: ${sid} (source: ${source || 'none'}, client: ${clientSlug || 'unknown'})`);
|
|
1092
1285
|
transports.set(sid, transport);
|
|
1093
1286
|
sessionApiKeys.set(sid, apiKey);
|
|
1094
1287
|
sessionUserIds.set(sid, userId);
|
|
1095
1288
|
sessionCreatedAt.set(sid, Date.now()); // C2: track creation time
|
|
1096
1289
|
if (source)
|
|
1097
1290
|
sessionSources.set(sid, source);
|
|
1291
|
+
if (clientSlug)
|
|
1292
|
+
sessionClients.set(sid, clientSlug);
|
|
1098
1293
|
if (sptInfo)
|
|
1099
1294
|
sessionSpts.set(sid, sptInfo);
|
|
1100
1295
|
}
|
|
@@ -1106,6 +1301,7 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1106
1301
|
sessionApiKeys.delete(sid);
|
|
1107
1302
|
sessionUserIds.delete(sid);
|
|
1108
1303
|
sessionSources.delete(sid);
|
|
1304
|
+
sessionClients.delete(sid);
|
|
1109
1305
|
sessionSpts.delete(sid);
|
|
1110
1306
|
sessionCreatedAt.delete(sid); // C2: clean up creation time
|
|
1111
1307
|
}
|
|
@@ -1113,11 +1309,41 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1113
1309
|
await server.connect(transport);
|
|
1114
1310
|
}
|
|
1115
1311
|
else {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1312
|
+
// Non-initialize POST without an existing sessionId:
|
|
1313
|
+
// Support stateless single-shot JSON-RPC execution (e.g. direct tools/list via curl or script)
|
|
1314
|
+
if (!isSpt && !apiKey && process.env.MCP_ALLOW_ANONYMOUS !== 'true') {
|
|
1315
|
+
return sendAuth401(res, {
|
|
1316
|
+
offeringSlug,
|
|
1317
|
+
message: 'Authentication required. Connect via OAuth, or use your personal connection URL or API key from https://app.fodda.ai.',
|
|
1318
|
+
invalidToken: false,
|
|
1319
|
+
id: body?.id ?? null,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
let sptInfo = null;
|
|
1323
|
+
if (isSpt) {
|
|
1324
|
+
if (process.env.ENABLE_SPT !== 'true') {
|
|
1325
|
+
return res.status(401).json({
|
|
1326
|
+
jsonrpc: '2.0',
|
|
1327
|
+
error: { code: -32000, message: 'SPT payment tokens are not accepted on this endpoint. Connect via OAuth at https://app.fodda.ai.' },
|
|
1328
|
+
id: body?.id ?? null,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
const v = await validateSpt(spt);
|
|
1332
|
+
if (!v.valid) {
|
|
1333
|
+
return res.status(402).json({
|
|
1334
|
+
jsonrpc: '2.0',
|
|
1335
|
+
error: { code: -32002, message: `SPT validation failed: ${v.error || 'invalid token'}` },
|
|
1336
|
+
id: body?.id ?? null,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
sptInfo = { token: spt, maxAmountCents: v.max_amount_cents, prices: v.prices };
|
|
1340
|
+
}
|
|
1341
|
+
const boundFoddaRequest = isSpt
|
|
1342
|
+
? ((m, p, _k, u, b, r, _s, sptArg) => foddaRequest(m, p, sptArg ? '' : internalKey, u, b, r, isInternalTest ? 'mcp-internal-test' : 'spt', sptArg, clientSlug || undefined))
|
|
1343
|
+
: ((m, p, k, u, b, r) => foddaRequest(m, p, k, u, b, r, source || undefined, undefined, clientSlug || undefined));
|
|
1344
|
+
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl, entryId, sptInfo ?? undefined, allowedTools, source || undefined);
|
|
1345
|
+
transport = new StreamableHTTPServerTransport();
|
|
1346
|
+
await server.connect(transport);
|
|
1121
1347
|
}
|
|
1122
1348
|
}
|
|
1123
1349
|
else {
|
|
@@ -1127,10 +1353,15 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1127
1353
|
id: null
|
|
1128
1354
|
});
|
|
1129
1355
|
}
|
|
1130
|
-
//
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1356
|
+
// Ensure Accept includes both application/json and text/event-stream (required by MCP SDK Streamable HTTP transport)
|
|
1357
|
+
const currentAccept = (req.headers['accept'] || '').toString();
|
|
1358
|
+
const needed = [];
|
|
1359
|
+
if (!currentAccept.includes('application/json'))
|
|
1360
|
+
needed.push('application/json');
|
|
1361
|
+
if (!currentAccept.includes('text/event-stream'))
|
|
1362
|
+
needed.push('text/event-stream');
|
|
1363
|
+
if (needed.length > 0) {
|
|
1364
|
+
req.headers['accept'] = currentAccept ? `${currentAccept}, ${needed.join(', ')}` : 'application/json, text/event-stream';
|
|
1134
1365
|
const idx = req.rawHeaders?.findIndex((h) => h.toLowerCase() === 'accept');
|
|
1135
1366
|
if (idx !== undefined && idx >= 0 && req.rawHeaders) {
|
|
1136
1367
|
req.rawHeaders[idx + 1] = req.headers['accept'];
|
|
@@ -1147,29 +1378,6 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1147
1378
|
res.status(500).json({ error: 'Internal server error' });
|
|
1148
1379
|
}
|
|
1149
1380
|
});
|
|
1150
|
-
// Legacy SSE transport
|
|
1151
|
-
app.get('/sse', async (req, res) => {
|
|
1152
|
-
const apiKey = req.query.api_key || '';
|
|
1153
|
-
const entryId = req.query.id || '';
|
|
1154
|
-
const isEmailId = entryId.includes('@') && entryId.includes('.');
|
|
1155
|
-
const userId = req.query.user_id
|
|
1156
|
-
|| req.headers['x-user-id']
|
|
1157
|
-
|| (isEmailId ? entryId : 'anonymous');
|
|
1158
|
-
const source = req.query.source || '';
|
|
1159
|
-
const sessionId = crypto.randomUUID();
|
|
1160
|
-
const transport = new SSEServerTransport('/messages', res);
|
|
1161
|
-
// Bind source parameter to foddaRequest to forward it upstream as X-Fodda-Source
|
|
1162
|
-
const boundFoddaRequest = source
|
|
1163
|
-
? ((m, p, k, u, b, r) => foddaRequest(m, p, k, u, b, r, source))
|
|
1164
|
-
: foddaRequest;
|
|
1165
|
-
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl);
|
|
1166
|
-
await server.connect(transport);
|
|
1167
|
-
console.error(`SSE session: ${sessionId}`);
|
|
1168
|
-
});
|
|
1169
|
-
app.post('/messages', async (req, res) => {
|
|
1170
|
-
// SSE message handler would go here
|
|
1171
|
-
res.status(404).json({ error: 'Use /mcp endpoint' });
|
|
1172
|
-
});
|
|
1173
1381
|
// Favicon — required for Anthropic Connectors Directory (Google favicon API)
|
|
1174
1382
|
const FAVICON_SVG = 'https://ucarecdn.com/e3ce77f2-661e-48a1-a294-c7c01039aed4/foddaminilogo.svg';
|
|
1175
1383
|
const FAVICON_PNG = 'https://ucarecdn.com/6e7893d7-6b14-426b-83bc-574a3f72d6bc/foddafavicon.png';
|