fodda-mcp 1.38.0 → 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 -91
- 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 +278 -83
- package/dist/brandTemplate.js.map +1 -1
- package/dist/catalogCache.d.ts +39 -0
- package/dist/catalogCache.d.ts.map +1 -1
- package/dist/catalogCache.js +293 -22
- package/dist/catalogCache.js.map +1 -1
- package/dist/coverageRelevance.d.ts +251 -0
- package/dist/coverageRelevance.d.ts.map +1 -0
- package/dist/coverageRelevance.js +1857 -0
- package/dist/coverageRelevance.js.map +1 -0
- 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 +442 -29
- 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 +411 -106
- package/dist/index.js.map +1 -1
- package/dist/pricingCache.d.ts.map +1 -1
- package/dist/pricingCache.js +55 -3
- 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 +2 -2
- package/dist/services/notifications.js +3 -3
- package/dist/services/notifications.js.map +1 -1
- package/dist/sessionTracker.d.ts +42 -3
- package/dist/sessionTracker.d.ts.map +1 -1
- package/dist/sessionTracker.js +261 -14
- 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 +153 -81
- 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.d.ts +2 -0
- package/dist/test_book_a_call_live.d.ts.map +1 -0
- package/dist/test_book_a_call_live.js +161 -0
- package/dist/test_book_a_call_live.js.map +1 -0
- package/dist/test_brand_intelligence_endpoint.js +2 -2
- 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.d.ts +12 -0
- package/dist/test_coverage_relevance.d.ts.map +1 -0
- package/dist/test_coverage_relevance.js +162 -0
- package/dist/test_coverage_relevance.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/test_dcr_and_legacy_deprecation.d.ts.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.js +143 -0
- package/dist/test_dcr_and_legacy_deprecation.js.map +1 -0
- 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_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 +51 -0
- package/dist/test_gap_alert.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/test_next_moves.d.ts.map +1 -0
- package/dist/test_next_moves.js +714 -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 +495 -0
- package/dist/test_next_moves_transcripts.js.map +1 -0
- 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_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_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 +4290 -711
- package/dist/toolHandlers.js.map +1 -1
- package/dist/tools.d.ts +22 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +29 -4
- package/dist/tools.js.map +1 -1
- package/dist/verify_tools_endpoint.js +19 -5
- 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 +3 -31
- package/dist/debug_tools.d.ts +0 -2
- package/dist/debug_tools.d.ts.map +0 -1
- package/dist/debug_tools.js +0 -19
- package/dist/debug_tools.js.map +0 -1
- package/dist/inspect_list_graphs.d.ts +0 -2
- package/dist/inspect_list_graphs.d.ts.map +0 -1
- package/dist/inspect_list_graphs.js +0 -25
- package/dist/inspect_list_graphs.js.map +0 -1
- package/dist/scratch_test.d.ts.map +0 -1
- 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/test_coverage_r2.d.ts +0 -2
- package/dist/test_coverage_r2.d.ts.map +0 -1
- package/dist/test_coverage_scratch.d.ts +0 -2
- package/dist/test_coverage_scratch.d.ts.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';
|
|
@@ -32,41 +31,183 @@ process.on('uncaughtException', (err) => {
|
|
|
32
31
|
});
|
|
33
32
|
const API_BASE_URL = process.env.FODDA_API_URL || 'https://api.fodda.ai';
|
|
34
33
|
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'https://www.fodda.ai';
|
|
35
|
-
const app = express();
|
|
34
|
+
export const app = express();
|
|
36
35
|
app.use(express.json({ limit: '512kb' }));
|
|
36
|
+
// Strip api_key from request logging globally before anything hits Cloud Run logs
|
|
37
|
+
app.use((req, _res, next) => {
|
|
38
|
+
if (req.url && req.url.includes('api_key=')) {
|
|
39
|
+
req.url = req.url.replace(/api_key=[^&]+/g, 'api_key=REDACTED');
|
|
40
|
+
}
|
|
41
|
+
next();
|
|
42
|
+
});
|
|
37
43
|
// CORS — minimal, matching test server
|
|
38
44
|
app.use((req, res, next) => {
|
|
39
45
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
40
46
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
41
|
-
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');
|
|
42
48
|
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
43
49
|
if (req.method === 'OPTIONS')
|
|
44
50
|
return res.status(204).end();
|
|
45
51
|
next();
|
|
46
52
|
});
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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'
|
|
56
145
|
});
|
|
57
146
|
});
|
|
58
|
-
|
|
147
|
+
// Deprecation middleware for legacy URL parameters (?api_key=... / ?user_id=...)
|
|
148
|
+
const LEGACY_DEPRECATION_PATHS = [
|
|
149
|
+
'/mcp', '/copilot', '/chatgpt',
|
|
150
|
+
'/brand-intelligence', '/topic-research', '/deep-research',
|
|
151
|
+
'/earnings-intelligence', '/expert-consult',
|
|
152
|
+
'/grok-brand-context', '/c/:token', '/c'
|
|
153
|
+
];
|
|
154
|
+
app.use(LEGACY_DEPRECATION_PATHS, (req, res, next) => {
|
|
155
|
+
if (req.query.api_key !== undefined || req.query.user_id !== undefined) {
|
|
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.';
|
|
174
|
+
const accept = req.headers['accept'] || '';
|
|
175
|
+
const contentType = req.headers['content-type'] || '';
|
|
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"`);
|
|
178
|
+
if (isJson && req.method === 'POST') {
|
|
179
|
+
const bodyId = (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null;
|
|
180
|
+
return res.status(401).json({
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
error: {
|
|
183
|
+
code: -32001,
|
|
184
|
+
message,
|
|
185
|
+
data: { docs: 'https://www.fodda.ai/connect' }
|
|
186
|
+
},
|
|
187
|
+
id: bodyId,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
191
|
+
return res.status(401).send(`${message}\n`);
|
|
192
|
+
}
|
|
193
|
+
next();
|
|
194
|
+
});
|
|
195
|
+
// OAuth discovery endpoints — Clerk-backed OAuth for the MCP Connectors Directory.
|
|
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.
|
|
198
|
+
const CLERK_ISSUER = process.env.CLERK_ISSUER_URL || 'https://clerk.fodda.ai';
|
|
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();
|
|
59
202
|
res.status(200).json({
|
|
60
|
-
resource:
|
|
203
|
+
resource: resourceUrl,
|
|
61
204
|
authorization_servers: [CLERK_ISSUER],
|
|
62
205
|
});
|
|
63
206
|
});
|
|
64
|
-
app.get('/.well-known/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// DCR not handled here — managed by Clerk dashboard
|
|
69
|
-
res.status(501).json({ error: 'Dynamic client registration is managed via the Clerk dashboard.' });
|
|
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);
|
|
70
211
|
});
|
|
71
212
|
// ---------------------------------------------------------------------------
|
|
72
213
|
// .well-known MCP Server Discovery Card & Pricing Metadata Tier
|
|
@@ -137,7 +278,11 @@ export const OFFERING_SCOPED_TOOLS = {
|
|
|
137
278
|
],
|
|
138
279
|
'expert-consult': [
|
|
139
280
|
'get_capabilities',
|
|
281
|
+
'find_expert',
|
|
282
|
+
'request_expert_intro',
|
|
140
283
|
'consult_analyst',
|
|
284
|
+
'consult_human_agent',
|
|
285
|
+
'verify_claim',
|
|
141
286
|
'list_analysts',
|
|
142
287
|
'request_deliverable',
|
|
143
288
|
'check_deliverable_status',
|
|
@@ -150,6 +295,64 @@ export const OFFERING_SCOPED_TOOLS = {
|
|
|
150
295
|
'get_my_account',
|
|
151
296
|
'generate_visual',
|
|
152
297
|
],
|
|
298
|
+
'copilot': [
|
|
299
|
+
'get_capabilities',
|
|
300
|
+
'search_graph',
|
|
301
|
+
'search_statistics',
|
|
302
|
+
'search_insights',
|
|
303
|
+
'get_validated_trends',
|
|
304
|
+
'brand_tracker',
|
|
305
|
+
'get_company_earnings',
|
|
306
|
+
'deep_research_topic',
|
|
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',
|
|
332
|
+
'consult_analyst',
|
|
333
|
+
'consult_human_agent',
|
|
334
|
+
'list_analysts',
|
|
335
|
+
'get_evidence',
|
|
336
|
+
'get_node',
|
|
337
|
+
'get_neighbors',
|
|
338
|
+
'get_label_values',
|
|
339
|
+
'list_graphs',
|
|
340
|
+
'get_my_account',
|
|
341
|
+
'read_url',
|
|
342
|
+
'get_supplemental_context',
|
|
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',
|
|
355
|
+
],
|
|
153
356
|
};
|
|
154
357
|
const OFFERING_CARD_METADATA = {
|
|
155
358
|
'brand-intelligence': {
|
|
@@ -177,6 +380,21 @@ const OFFERING_CARD_METADATA = {
|
|
|
177
380
|
title: 'Fodda Synthetic Expert Consult',
|
|
178
381
|
description: 'Consult synthetic industry experts grounded in PSFK trend graphs with citable analyst perspectives.',
|
|
179
382
|
},
|
|
383
|
+
'copilot': {
|
|
384
|
+
name: 'ai.fodda/copilot',
|
|
385
|
+
title: 'Fodda Copilot Market Intelligence',
|
|
386
|
+
description: 'Curated market intelligence, trend insights & expert evidence for Microsoft Copilot Studio agents.',
|
|
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
|
+
},
|
|
180
398
|
};
|
|
181
399
|
app.get([
|
|
182
400
|
'/.well-known/mcp-server.json', '/.well-known/mcp',
|
|
@@ -185,6 +403,9 @@ app.get([
|
|
|
185
403
|
'/.well-known/deep-research', '/.well-known/deep-research.json',
|
|
186
404
|
'/.well-known/earnings-intelligence', '/.well-known/earnings-intelligence.json',
|
|
187
405
|
'/.well-known/expert-consult', '/.well-known/expert-consult.json',
|
|
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',
|
|
188
409
|
], (req, res) => {
|
|
189
410
|
res.setHeader('Content-Type', 'application/json');
|
|
190
411
|
const matchedOffering = Object.keys(OFFERING_CARD_METADATA).find(slug => req.path.includes(slug));
|
|
@@ -205,7 +426,7 @@ app.get([
|
|
|
205
426
|
pricingTier: {
|
|
206
427
|
structure: 'free',
|
|
207
428
|
substance: 'metered',
|
|
208
|
-
tokenModel: '
|
|
429
|
+
tokenModel: 'Metered per API call — see https://fodda.ai/pricing',
|
|
209
430
|
},
|
|
210
431
|
resourceSchemes: [
|
|
211
432
|
'fodda://expert/{slug}/insight/{id}',
|
|
@@ -213,7 +434,6 @@ app.get([
|
|
|
213
434
|
],
|
|
214
435
|
endpoints: {
|
|
215
436
|
mcpStreamableHttp: `${getServiceUrl()}/${matchedOffering}`,
|
|
216
|
-
mcpSse: `${getServiceUrl()}/sse`,
|
|
217
437
|
telemetry: `${getServiceUrl()}/telemetry`,
|
|
218
438
|
feedback: `${getServiceUrl()}/v1/feedback`,
|
|
219
439
|
},
|
|
@@ -234,7 +454,7 @@ app.get([
|
|
|
234
454
|
pricingTier: {
|
|
235
455
|
structure: 'free',
|
|
236
456
|
substance: 'metered',
|
|
237
|
-
tokenModel: '
|
|
457
|
+
tokenModel: 'Metered per API call — see https://fodda.ai/pricing',
|
|
238
458
|
},
|
|
239
459
|
resourceSchemes: [
|
|
240
460
|
'fodda://expert/{slug}/insight/{id}',
|
|
@@ -242,7 +462,6 @@ app.get([
|
|
|
242
462
|
],
|
|
243
463
|
endpoints: {
|
|
244
464
|
mcpStreamableHttp: `${getServiceUrl()}/mcp`,
|
|
245
|
-
mcpSse: `${getServiceUrl()}/sse`,
|
|
246
465
|
telemetry: `${getServiceUrl()}/telemetry`,
|
|
247
466
|
feedback: `${getServiceUrl()}/v1/feedback`,
|
|
248
467
|
},
|
|
@@ -292,12 +511,56 @@ const transports = new Map();
|
|
|
292
511
|
const sessionApiKeys = new Map();
|
|
293
512
|
const sessionUserIds = new Map();
|
|
294
513
|
const sessionSources = new Map();
|
|
514
|
+
export const sessionClients = new Map();
|
|
295
515
|
// SPT-paying anonymous sessions (no API key). Holds the token + the connect-time
|
|
296
516
|
// validate result (cap + price map) so per-task coverage can be checked locally.
|
|
297
517
|
const sessionSpts = new Map();
|
|
298
518
|
// Track session creation time in our own Map (transport._createdAt is a private
|
|
299
519
|
// field that is never set by the SDK — reading it always yields undefined).
|
|
300
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
|
+
}
|
|
301
564
|
// ---------------------------------------------------------------------------
|
|
302
565
|
// Periodic cleanup — prevent unbounded memory growth
|
|
303
566
|
// ---------------------------------------------------------------------------
|
|
@@ -318,44 +581,40 @@ setInterval(() => {
|
|
|
318
581
|
sessionApiKeys.delete(sid);
|
|
319
582
|
sessionUserIds.delete(sid);
|
|
320
583
|
sessionSources.delete(sid);
|
|
584
|
+
sessionClients.delete(sid);
|
|
321
585
|
sessionSpts.delete(sid);
|
|
322
586
|
sessionCreatedAt.delete(sid);
|
|
323
587
|
}
|
|
324
588
|
}
|
|
325
589
|
}, CLEANUP_INTERVAL_MS).unref();
|
|
326
590
|
// ---------------------------------------------------------------------------
|
|
327
|
-
// Service URL helper
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
// M2: FODDA_SERVICE_URL must be set in Cloud Run env vars.
|
|
330
|
-
// Fallback to localhost for local dev only. The hardcoded project hash
|
|
331
|
-
// was removed — if it was being used, the URL was silently wrong.
|
|
332
|
-
function getServiceUrl() {
|
|
333
|
-
if (process.env.FODDA_SERVICE_URL)
|
|
334
|
-
return process.env.FODDA_SERVICE_URL;
|
|
335
|
-
if (process.env.NODE_ENV === 'production' || process.env.K_SERVICE) {
|
|
336
|
-
return 'https://mcp.fodda.ai';
|
|
337
|
-
}
|
|
338
|
-
return `http://localhost:${process.env.PORT || 8080}`;
|
|
339
|
-
}
|
|
340
|
-
// ---------------------------------------------------------------------------
|
|
341
591
|
// Authenticated API caller — checks query cache first
|
|
342
592
|
// ---------------------------------------------------------------------------
|
|
343
593
|
/**
|
|
344
594
|
* Make an authenticated request to the Fodda API.
|
|
345
595
|
* Checks the query cache first; stores responses on cache miss.
|
|
346
596
|
*/
|
|
347
|
-
|
|
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) {
|
|
348
604
|
// ── Cache check ──
|
|
349
605
|
const cached = cacheGet(method, path, body);
|
|
350
606
|
if (cached !== null)
|
|
351
607
|
return cached;
|
|
352
608
|
const timestamp = Date.now().toString();
|
|
353
609
|
const headers = {
|
|
354
|
-
'X-User-Id': userId,
|
|
355
610
|
'X-Fodda-Timestamp': timestamp,
|
|
356
611
|
'X-Fodda-Billing': 'mcp-orchestrated', // Tells API to skip per-call billing — MCP charges lump sum via meter
|
|
357
612
|
'Content-Type': 'application/json',
|
|
358
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
|
+
}
|
|
359
618
|
// SPT settlement: the Shared Payment Token is the payer (Authorization Bearer), no X-API-Key.
|
|
360
619
|
if (spt) {
|
|
361
620
|
headers['Authorization'] = `Bearer ${spt}`;
|
|
@@ -367,6 +626,8 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
367
626
|
headers['X-Request-Id'] = requestId;
|
|
368
627
|
if (source)
|
|
369
628
|
headers['X-Fodda-Source'] = source;
|
|
629
|
+
if (client)
|
|
630
|
+
headers['X-Fodda-Client'] = client;
|
|
370
631
|
// HMAC sign the request
|
|
371
632
|
const secret = process.env.FODDA_MCP_SECRET;
|
|
372
633
|
if (secret) {
|
|
@@ -376,12 +637,12 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
376
637
|
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
377
638
|
headers['X-Fodda-Signature'] = signature;
|
|
378
639
|
}
|
|
379
|
-
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);
|
|
380
641
|
const url = `${baseUrl}${path}`;
|
|
381
642
|
// Base timeout: 30s aligns with MCP client expectations.
|
|
382
|
-
// Extended to
|
|
383
|
-
const AXIOS_TIMEOUT_MS =
|
|
384
|
-
?
|
|
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)
|
|
645
|
+
? 90000
|
|
385
646
|
: /\/supplemental\//.test(path)
|
|
386
647
|
? 35000
|
|
387
648
|
: 30000;
|
|
@@ -406,6 +667,12 @@ async function foddaRequest(method, path, apiKey, userId, body, requestId, sourc
|
|
|
406
667
|
const overageTokens = response.headers['x-usage-overage-tokens'];
|
|
407
668
|
if (overageTokens)
|
|
408
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);
|
|
409
676
|
}
|
|
410
677
|
// ── Billing-mode trust check (Option C) ──
|
|
411
678
|
// Every MCP request claims mcp-orchestrated; the API echoes the EFFECTIVE mode.
|
|
@@ -548,7 +815,7 @@ async function waverunnerRequest(interactionType, tokenCost, userApiKey, userId,
|
|
|
548
815
|
// ---------------------------------------------------------------------------
|
|
549
816
|
// Browser Landing Page — catch humans who paste the MCP URL into a browser
|
|
550
817
|
// ---------------------------------------------------------------------------
|
|
551
|
-
app.get(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult'], (req, res, next) => {
|
|
818
|
+
app.get(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult', '/copilot'], (req, res, next) => {
|
|
552
819
|
// Only intercept browser requests (Accept: text/html).
|
|
553
820
|
// MCP SDK clients send application/json or text/event-stream, so they
|
|
554
821
|
// fall through to the app.all('/mcp') transport handler below.
|
|
@@ -842,7 +1109,7 @@ export async function resolveMcpToken(token, websiteBaseUrl) {
|
|
|
842
1109
|
// ---------------------------------------------------------------------------
|
|
843
1110
|
// MCP Transport Handler
|
|
844
1111
|
// ---------------------------------------------------------------------------
|
|
845
|
-
app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/earnings-intelligence', '/expert-consult', '/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) => {
|
|
846
1113
|
try {
|
|
847
1114
|
const sessionId = req.headers['mcp-session-id'];
|
|
848
1115
|
let transport;
|
|
@@ -859,9 +1126,17 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
859
1126
|
|| '';
|
|
860
1127
|
const isSpt = !!spt;
|
|
861
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));
|
|
862
1137
|
// Clerk OAuth tokens may be JWTs (eyJ...), opaque tokens (oat_...), session tokens (sess_...), or custom OAuth tokens.
|
|
863
|
-
// If it's not a standard sk_live_ / sk_trial_ key, send it to clerk-resolve.
|
|
864
|
-
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_');
|
|
865
1140
|
if (isClerkJwt) {
|
|
866
1141
|
// Attempt Clerk JWT → Fodda API key resolution
|
|
867
1142
|
try {
|
|
@@ -875,9 +1150,10 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
875
1150
|
req.__resolvedUserId = resolveResp.data.user_id || 'oauth_user';
|
|
876
1151
|
}
|
|
877
1152
|
else {
|
|
878
|
-
return res
|
|
879
|
-
|
|
880
|
-
|
|
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,
|
|
881
1157
|
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
882
1158
|
});
|
|
883
1159
|
}
|
|
@@ -891,9 +1167,10 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
891
1167
|
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
892
1168
|
});
|
|
893
1169
|
}
|
|
894
|
-
return res
|
|
895
|
-
|
|
896
|
-
|
|
1170
|
+
return sendAuth401(res, {
|
|
1171
|
+
offeringSlug,
|
|
1172
|
+
message: 'OAuth token validation failed. Please reconnect at https://app.fodda.ai.',
|
|
1173
|
+
invalidToken: true,
|
|
897
1174
|
id: (req.body && typeof req.body === 'object' && 'id' in req.body) ? req.body.id : null,
|
|
898
1175
|
});
|
|
899
1176
|
}
|
|
@@ -911,45 +1188,61 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
911
1188
|
}
|
|
912
1189
|
catch (err) {
|
|
913
1190
|
console.error(`[token-resolver] Failed to resolve connection token:`, err.message);
|
|
914
|
-
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
|
+
});
|
|
915
1197
|
}
|
|
916
1198
|
}
|
|
917
1199
|
// Extract API key, userId, and entry ID from URL or headers.
|
|
918
|
-
// Priority: resolved OAuth key → X-API-Key header → Authorization Bearer (if non-Clerk) → 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).
|
|
919
1201
|
const resolvedOAuthKey = req.__resolvedApiKey || '';
|
|
920
1202
|
const resolvedOAuthUser = req.__resolvedUserId || '';
|
|
921
1203
|
const apiKey = isSpt ? '' : (resolvedOAuthKey
|
|
922
1204
|
|| req.headers['x-api-key']
|
|
923
|
-
|| (!isClerkJwt ?
|
|
1205
|
+
|| (!isClerkJwt ? rawBearer : '')
|
|
1206
|
+
|| (isInternalAuth ? (internalKey || 'sk_internal_service') : '')
|
|
924
1207
|
|| resolvedApiKey
|
|
925
1208
|
|| req.query.api_key
|
|
926
1209
|
|| '');
|
|
927
1210
|
const entryId = resolvedEntryId || req.query.id || '';
|
|
928
1211
|
// If id looks like an email and no explicit user_id, use it as userId for tracking + signup
|
|
929
1212
|
const isEmailId = entryId.includes('@') && entryId.includes('.');
|
|
930
|
-
|
|
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
|
|
931
1215
|
const userId = isSpt ? 'spt_agent' : (resolvedOAuthUser
|
|
932
1216
|
|| resolvedEmail
|
|
1217
|
+
|| headerUserId
|
|
933
1218
|
|| req.query.user_id
|
|
934
|
-
|| req.headers['x-user-id']
|
|
935
1219
|
|| (isEmailId ? entryId : 'anonymous'));
|
|
936
|
-
const
|
|
937
|
-
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));
|
|
938
1228
|
if (sessionId && transports.has(sessionId)) {
|
|
939
1229
|
transport = transports.get(sessionId);
|
|
940
1230
|
}
|
|
941
1231
|
else if (!sessionId && req.method === 'POST') {
|
|
942
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' : '');
|
|
943
1236
|
if (body?.method === 'initialize') {
|
|
944
|
-
// Directory connector policy:
|
|
1237
|
+
// Directory connector policy: All offering routes require authentication — Clerk OAuth,
|
|
945
1238
|
// an API key, or a /c/<token> connection URL. An anonymous handshake gets
|
|
946
1239
|
// 401 + WWW-Authenticate (RFC 9728) so MCP clients auto-start the OAuth
|
|
947
1240
|
// flow. Set MCP_ALLOW_ANONYMOUS=true to re-open the anonymous trial lane.
|
|
948
|
-
if (
|
|
949
|
-
res
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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,
|
|
953
1246
|
id: body?.id ?? null,
|
|
954
1247
|
});
|
|
955
1248
|
}
|
|
@@ -981,23 +1274,22 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
981
1274
|
// Wrap foddaRequest. SPT sessions authenticate the internal fan-out with the
|
|
982
1275
|
// internal service key (the SPT is spent ONCE at settlement, never on fan-out);
|
|
983
1276
|
// otherwise bake in source attribution.
|
|
984
|
-
const internalKey = process.env.FODDA_INTERNAL_API_KEY || '';
|
|
985
1277
|
const boundFoddaRequest = isSpt
|
|
986
|
-
? ((m, p, _k, u, b, r, _s, sptArg) => foddaRequest(m, p, sptArg ? '' : internalKey, u, b, r, 'spt', sptArg))
|
|
987
|
-
: (source
|
|
988
|
-
|
|
989
|
-
: foddaRequest);
|
|
990
|
-
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);
|
|
991
1281
|
transport = new StreamableHTTPServerTransport({
|
|
992
1282
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
993
1283
|
onsessioninitialized: (sid) => {
|
|
994
|
-
console.error(`Session created: ${sid}`);
|
|
1284
|
+
console.error(`Session created: ${sid} (source: ${source || 'none'}, client: ${clientSlug || 'unknown'})`);
|
|
995
1285
|
transports.set(sid, transport);
|
|
996
1286
|
sessionApiKeys.set(sid, apiKey);
|
|
997
1287
|
sessionUserIds.set(sid, userId);
|
|
998
1288
|
sessionCreatedAt.set(sid, Date.now()); // C2: track creation time
|
|
999
1289
|
if (source)
|
|
1000
1290
|
sessionSources.set(sid, source);
|
|
1291
|
+
if (clientSlug)
|
|
1292
|
+
sessionClients.set(sid, clientSlug);
|
|
1001
1293
|
if (sptInfo)
|
|
1002
1294
|
sessionSpts.set(sid, sptInfo);
|
|
1003
1295
|
}
|
|
@@ -1009,6 +1301,7 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1009
1301
|
sessionApiKeys.delete(sid);
|
|
1010
1302
|
sessionUserIds.delete(sid);
|
|
1011
1303
|
sessionSources.delete(sid);
|
|
1304
|
+
sessionClients.delete(sid);
|
|
1012
1305
|
sessionSpts.delete(sid);
|
|
1013
1306
|
sessionCreatedAt.delete(sid); // C2: clean up creation time
|
|
1014
1307
|
}
|
|
@@ -1016,11 +1309,41 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1016
1309
|
await server.connect(transport);
|
|
1017
1310
|
}
|
|
1018
1311
|
else {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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);
|
|
1024
1347
|
}
|
|
1025
1348
|
}
|
|
1026
1349
|
else {
|
|
@@ -1030,10 +1353,15 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1030
1353
|
id: null
|
|
1031
1354
|
});
|
|
1032
1355
|
}
|
|
1033
|
-
//
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
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';
|
|
1037
1365
|
const idx = req.rawHeaders?.findIndex((h) => h.toLowerCase() === 'accept');
|
|
1038
1366
|
if (idx !== undefined && idx >= 0 && req.rawHeaders) {
|
|
1039
1367
|
req.rawHeaders[idx + 1] = req.headers['accept'];
|
|
@@ -1050,29 +1378,6 @@ app.all(['/mcp', '/brand-intelligence', '/topic-research', '/deep-research', '/e
|
|
|
1050
1378
|
res.status(500).json({ error: 'Internal server error' });
|
|
1051
1379
|
}
|
|
1052
1380
|
});
|
|
1053
|
-
// Legacy SSE transport
|
|
1054
|
-
app.get('/sse', async (req, res) => {
|
|
1055
|
-
const apiKey = req.query.api_key || '';
|
|
1056
|
-
const entryId = req.query.id || '';
|
|
1057
|
-
const isEmailId = entryId.includes('@') && entryId.includes('.');
|
|
1058
|
-
const userId = req.query.user_id
|
|
1059
|
-
|| req.headers['x-user-id']
|
|
1060
|
-
|| (isEmailId ? entryId : 'anonymous');
|
|
1061
|
-
const source = req.query.source || '';
|
|
1062
|
-
const sessionId = crypto.randomUUID();
|
|
1063
|
-
const transport = new SSEServerTransport('/messages', res);
|
|
1064
|
-
// Bind source parameter to foddaRequest to forward it upstream as X-Fodda-Source
|
|
1065
|
-
const boundFoddaRequest = source
|
|
1066
|
-
? ((m, p, k, u, b, r) => foddaRequest(m, p, k, u, b, r, source))
|
|
1067
|
-
: foddaRequest;
|
|
1068
|
-
const server = await createServer(apiKey, userId, boundFoddaRequest, waverunnerRequest, storeWidget, getServiceUrl);
|
|
1069
|
-
await server.connect(transport);
|
|
1070
|
-
console.error(`SSE session: ${sessionId}`);
|
|
1071
|
-
});
|
|
1072
|
-
app.post('/messages', async (req, res) => {
|
|
1073
|
-
// SSE message handler would go here
|
|
1074
|
-
res.status(404).json({ error: 'Use /mcp endpoint' });
|
|
1075
|
-
});
|
|
1076
1381
|
// Favicon — required for Anthropic Connectors Directory (Google favicon API)
|
|
1077
1382
|
const FAVICON_SVG = 'https://ucarecdn.com/e3ce77f2-661e-48a1-a294-c7c01039aed4/foddaminilogo.svg';
|
|
1078
1383
|
const FAVICON_PNG = 'https://ucarecdn.com/6e7893d7-6b14-426b-83bc-574a3f72d6bc/foddafavicon.png';
|