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.
Files changed (346) hide show
  1. package/README.md +14 -91
  2. package/dist/agents/fodda-researcher/skills.d.ts +1 -1
  3. package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
  4. package/dist/agents/fodda-researcher/skills.js +1 -1
  5. package/dist/brandTemplate.d.ts +1 -1
  6. package/dist/brandTemplate.d.ts.map +1 -1
  7. package/dist/brandTemplate.js +278 -83
  8. package/dist/brandTemplate.js.map +1 -1
  9. package/dist/catalogCache.d.ts +39 -0
  10. package/dist/catalogCache.d.ts.map +1 -1
  11. package/dist/catalogCache.js +293 -22
  12. package/dist/catalogCache.js.map +1 -1
  13. package/dist/coverageRelevance.d.ts +251 -0
  14. package/dist/coverageRelevance.d.ts.map +1 -0
  15. package/dist/coverageRelevance.js +1857 -0
  16. package/dist/coverageRelevance.js.map +1 -0
  17. package/dist/editorialFill.d.ts +5 -0
  18. package/dist/editorialFill.d.ts.map +1 -1
  19. package/dist/editorialFill.js +1 -1
  20. package/dist/editorialFill.js.map +1 -1
  21. package/dist/enrichment.d.ts +26 -0
  22. package/dist/enrichment.d.ts.map +1 -1
  23. package/dist/enrichment.js +442 -29
  24. package/dist/enrichment.js.map +1 -1
  25. package/dist/errorHandling.d.ts +35 -7
  26. package/dist/errorHandling.d.ts.map +1 -1
  27. package/dist/errorHandling.js +225 -155
  28. package/dist/errorHandling.js.map +1 -1
  29. package/dist/index.d.ts +25 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +411 -106
  32. package/dist/index.js.map +1 -1
  33. package/dist/pricingCache.d.ts.map +1 -1
  34. package/dist/pricingCache.js +55 -3
  35. package/dist/pricingCache.js.map +1 -1
  36. package/dist/queryCache.d.ts.map +1 -1
  37. package/dist/queryCache.js +2 -0
  38. package/dist/queryCache.js.map +1 -1
  39. package/dist/reportBriefing.d.ts +89 -0
  40. package/dist/reportBriefing.d.ts.map +1 -0
  41. package/dist/reportBriefing.js +662 -0
  42. package/dist/reportBriefing.js.map +1 -0
  43. package/dist/searchTemplate.d.ts.map +1 -1
  44. package/dist/searchTemplate.js +65 -53
  45. package/dist/searchTemplate.js.map +1 -1
  46. package/dist/services/graphRouter.d.ts +7 -0
  47. package/dist/services/graphRouter.d.ts.map +1 -0
  48. package/dist/services/graphRouter.js +7 -0
  49. package/dist/services/graphRouter.js.map +1 -0
  50. package/dist/services/notifications.d.ts +2 -2
  51. package/dist/services/notifications.js +3 -3
  52. package/dist/services/notifications.js.map +1 -1
  53. package/dist/sessionTracker.d.ts +42 -3
  54. package/dist/sessionTracker.d.ts.map +1 -1
  55. package/dist/sessionTracker.js +261 -14
  56. package/dist/sessionTracker.js.map +1 -1
  57. package/dist/skillClient.d.ts.map +1 -1
  58. package/dist/skillClient.js +11 -6
  59. package/dist/skillClient.js.map +1 -1
  60. package/dist/svgVisuals.d.ts +17 -19
  61. package/dist/svgVisuals.d.ts.map +1 -1
  62. package/dist/svgVisuals.js +189 -233
  63. package/dist/svgVisuals.js.map +1 -1
  64. package/dist/systemPrompt.d.ts +7 -2
  65. package/dist/systemPrompt.d.ts.map +1 -1
  66. package/dist/systemPrompt.js +153 -81
  67. package/dist/systemPrompt.js.map +1 -1
  68. package/dist/telemetry.d.ts.map +1 -1
  69. package/dist/telemetry.js +15 -0
  70. package/dist/telemetry.js.map +1 -1
  71. package/dist/test_adjacent_trends_polymorphic.d.ts +2 -0
  72. package/dist/test_adjacent_trends_polymorphic.d.ts.map +1 -0
  73. package/dist/test_adjacent_trends_polymorphic.js +154 -0
  74. package/dist/test_adjacent_trends_polymorphic.js.map +1 -0
  75. package/dist/test_book_a_call_live.d.ts +2 -0
  76. package/dist/test_book_a_call_live.d.ts.map +1 -0
  77. package/dist/test_book_a_call_live.js +161 -0
  78. package/dist/test_book_a_call_live.js.map +1 -0
  79. package/dist/test_brand_intelligence_endpoint.js +2 -2
  80. package/dist/test_byo_mcp_submission_clarity.d.ts +2 -0
  81. package/dist/test_byo_mcp_submission_clarity.d.ts.map +1 -0
  82. package/dist/test_byo_mcp_submission_clarity.js +142 -0
  83. package/dist/test_byo_mcp_submission_clarity.js.map +1 -0
  84. package/dist/test_capabilities_guidance.d.ts +2 -0
  85. package/dist/test_capabilities_guidance.d.ts.map +1 -0
  86. package/dist/test_capabilities_guidance.js +372 -0
  87. package/dist/test_capabilities_guidance.js.map +1 -0
  88. package/dist/test_client_provenance.d.ts +2 -0
  89. package/dist/test_client_provenance.d.ts.map +1 -0
  90. package/dist/test_client_provenance.js +77 -0
  91. package/dist/test_client_provenance.js.map +1 -0
  92. package/dist/test_connection_surface_cleanup.d.ts +2 -0
  93. package/dist/test_connection_surface_cleanup.d.ts.map +1 -0
  94. package/dist/test_connection_surface_cleanup.js +203 -0
  95. package/dist/test_connection_surface_cleanup.js.map +1 -0
  96. package/dist/test_consult_routing.d.ts +2 -0
  97. package/dist/test_consult_routing.d.ts.map +1 -0
  98. package/dist/test_consult_routing.js +86 -0
  99. package/dist/test_consult_routing.js.map +1 -0
  100. package/dist/test_consult_split.d.ts +2 -0
  101. package/dist/test_consult_split.d.ts.map +1 -0
  102. package/dist/test_consult_split.js +30 -0
  103. package/dist/test_consult_split.js.map +1 -0
  104. package/dist/test_coverage_relevance.d.ts +12 -0
  105. package/dist/test_coverage_relevance.d.ts.map +1 -0
  106. package/dist/test_coverage_relevance.js +162 -0
  107. package/dist/test_coverage_relevance.js.map +1 -0
  108. package/dist/test_coverage_replay.d.ts +2 -0
  109. package/dist/test_coverage_replay.d.ts.map +1 -0
  110. package/dist/test_coverage_replay.js +170 -0
  111. package/dist/test_coverage_replay.js.map +1 -0
  112. package/dist/test_dcr_and_legacy_deprecation.d.ts +2 -0
  113. package/dist/test_dcr_and_legacy_deprecation.d.ts.map +1 -0
  114. package/dist/test_dcr_and_legacy_deprecation.js +143 -0
  115. package/dist/test_dcr_and_legacy_deprecation.js.map +1 -0
  116. package/dist/test_demand_signal_logging.d.ts +2 -0
  117. package/dist/test_demand_signal_logging.d.ts.map +1 -0
  118. package/dist/test_demand_signal_logging.js +92 -0
  119. package/dist/test_demand_signal_logging.js.map +1 -0
  120. package/dist/test_earnings_activity_scope.d.ts +5 -0
  121. package/dist/test_earnings_activity_scope.d.ts.map +1 -0
  122. package/dist/test_earnings_activity_scope.js +101 -0
  123. package/dist/test_earnings_activity_scope.js.map +1 -0
  124. package/dist/test_earnings_divergence_live.d.ts +5 -0
  125. package/dist/test_earnings_divergence_live.d.ts.map +1 -0
  126. package/dist/test_earnings_divergence_live.js +135 -0
  127. package/dist/test_earnings_divergence_live.js.map +1 -0
  128. package/dist/test_earnings_hygiene_and_binding.d.ts +5 -0
  129. package/dist/test_earnings_hygiene_and_binding.d.ts.map +1 -0
  130. package/dist/test_earnings_hygiene_and_binding.js +104 -0
  131. package/dist/test_earnings_hygiene_and_binding.js.map +1 -0
  132. package/dist/test_exec_quotes_render.d.ts +2 -0
  133. package/dist/test_exec_quotes_render.d.ts.map +1 -0
  134. package/dist/test_exec_quotes_render.js +86 -0
  135. package/dist/test_exec_quotes_render.js.map +1 -0
  136. package/dist/test_expert_intro.d.ts +2 -0
  137. package/dist/test_expert_intro.d.ts.map +1 -0
  138. package/dist/test_expert_intro.js +57 -0
  139. package/dist/test_expert_intro.js.map +1 -0
  140. package/dist/test_expert_layer.d.ts +11 -0
  141. package/dist/test_expert_layer.d.ts.map +1 -0
  142. package/dist/test_expert_layer.js +356 -0
  143. package/dist/test_expert_layer.js.map +1 -0
  144. package/dist/test_financial_snapshot_render.d.ts +2 -0
  145. package/dist/test_financial_snapshot_render.d.ts.map +1 -0
  146. package/dist/test_financial_snapshot_render.js +167 -0
  147. package/dist/test_financial_snapshot_render.js.map +1 -0
  148. package/dist/test_gap_alert.d.ts +12 -0
  149. package/dist/test_gap_alert.d.ts.map +1 -0
  150. package/dist/test_gap_alert.js +51 -0
  151. package/dist/test_gap_alert.js.map +1 -0
  152. package/dist/test_grok_offering_surfaces.d.ts +2 -0
  153. package/dist/test_grok_offering_surfaces.d.ts.map +1 -0
  154. package/dist/test_grok_offering_surfaces.js +155 -0
  155. package/dist/test_grok_offering_surfaces.js.map +1 -0
  156. package/dist/test_human_agent_live.d.ts +2 -0
  157. package/dist/test_human_agent_live.d.ts.map +1 -0
  158. package/dist/test_human_agent_live.js +48 -0
  159. package/dist/test_human_agent_live.js.map +1 -0
  160. package/dist/test_identity_gap.d.ts +2 -0
  161. package/dist/test_identity_gap.d.ts.map +1 -0
  162. package/dist/test_identity_gap.js +171 -0
  163. package/dist/test_identity_gap.js.map +1 -0
  164. package/dist/test_intelligence_and_verifier.d.ts +17 -0
  165. package/dist/test_intelligence_and_verifier.d.ts.map +1 -0
  166. package/dist/test_intelligence_and_verifier.js +162 -0
  167. package/dist/test_intelligence_and_verifier.js.map +1 -0
  168. package/dist/test_latency_mcp_brief5.d.ts +12 -0
  169. package/dist/test_latency_mcp_brief5.d.ts.map +1 -0
  170. package/dist/test_latency_mcp_brief5.js +200 -0
  171. package/dist/test_latency_mcp_brief5.js.map +1 -0
  172. package/dist/test_limit_and_credit_card_capture.d.ts +21 -0
  173. package/dist/test_limit_and_credit_card_capture.d.ts.map +1 -0
  174. package/dist/test_limit_and_credit_card_capture.js +216 -0
  175. package/dist/test_limit_and_credit_card_capture.js.map +1 -0
  176. package/dist/test_list_graphs_topics.js +1 -1
  177. package/dist/test_list_graphs_topics.js.map +1 -1
  178. package/dist/test_live_deployed_mcp_search.d.ts +2 -0
  179. package/dist/test_live_deployed_mcp_search.d.ts.map +1 -0
  180. package/dist/test_live_deployed_mcp_search.js +85 -0
  181. package/dist/test_live_deployed_mcp_search.js.map +1 -0
  182. package/dist/test_live_deployed_split.d.ts +2 -0
  183. package/dist/test_live_deployed_split.d.ts.map +1 -0
  184. package/dist/test_live_deployed_split.js +72 -0
  185. package/dist/test_live_deployed_split.js.map +1 -0
  186. package/dist/test_live_endpoint_runner.js +2 -2
  187. package/dist/test_live_endpoint_runner.js.map +1 -1
  188. package/dist/test_live_mcp.js +1 -1
  189. package/dist/test_live_mcp.js.map +1 -1
  190. package/dist/test_live_neil_widget.d.ts +2 -0
  191. package/dist/test_live_neil_widget.d.ts.map +1 -0
  192. package/dist/test_live_neil_widget.js +84 -0
  193. package/dist/test_live_neil_widget.js.map +1 -0
  194. package/dist/test_longpoll_status.d.ts +2 -0
  195. package/dist/test_longpoll_status.d.ts.map +1 -0
  196. package/dist/test_longpoll_status.js +117 -0
  197. package/dist/test_longpoll_status.js.map +1 -0
  198. package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts +2 -0
  199. package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts.map +1 -0
  200. package/dist/test_mcp_find_expert_and_vocabulary_routing.js +130 -0
  201. package/dist/test_mcp_find_expert_and_vocabulary_routing.js.map +1 -0
  202. package/dist/test_next_moves.d.ts +2 -0
  203. package/dist/test_next_moves.d.ts.map +1 -0
  204. package/dist/test_next_moves.js +714 -0
  205. package/dist/test_next_moves.js.map +1 -0
  206. package/dist/test_next_moves_transcripts.d.ts +2 -0
  207. package/dist/test_next_moves_transcripts.d.ts.map +1 -0
  208. package/dist/test_next_moves_transcripts.js +495 -0
  209. package/dist/test_next_moves_transcripts.js.map +1 -0
  210. package/dist/test_on_request_caller_identity.d.ts +13 -0
  211. package/dist/test_on_request_caller_identity.d.ts.map +1 -0
  212. package/dist/test_on_request_caller_identity.js +131 -0
  213. package/dist/test_on_request_caller_identity.js.map +1 -0
  214. package/dist/test_on_request_intercept.d.ts +10 -0
  215. package/dist/test_on_request_intercept.d.ts.map +1 -0
  216. package/dist/test_on_request_intercept.js +95 -0
  217. package/dist/test_on_request_intercept.js.map +1 -0
  218. package/dist/test_onboarding_consent_and_errors.d.ts +2 -0
  219. package/dist/test_onboarding_consent_and_errors.d.ts.map +1 -0
  220. package/dist/test_onboarding_consent_and_errors.js +110 -0
  221. package/dist/test_onboarding_consent_and_errors.js.map +1 -0
  222. package/dist/test_onboarding_live_scenarios.d.ts +2 -0
  223. package/dist/test_onboarding_live_scenarios.d.ts.map +1 -0
  224. package/dist/test_onboarding_live_scenarios.js +189 -0
  225. package/dist/test_onboarding_live_scenarios.js.map +1 -0
  226. package/dist/test_referral_live.d.ts +2 -0
  227. package/dist/test_referral_live.d.ts.map +1 -0
  228. package/dist/test_referral_live.js +33 -0
  229. package/dist/test_referral_live.js.map +1 -0
  230. package/dist/test_referral_piers.d.ts +2 -0
  231. package/dist/test_referral_piers.d.ts.map +1 -0
  232. package/dist/test_referral_piers.js +33 -0
  233. package/dist/test_referral_piers.js.map +1 -0
  234. package/dist/test_report_editorial_briefing.d.ts +21 -0
  235. package/dist/test_report_editorial_briefing.d.ts.map +1 -0
  236. package/dist/test_report_editorial_briefing.js +356 -0
  237. package/dist/test_report_editorial_briefing.js.map +1 -0
  238. package/dist/test_search_graph_quality.d.ts +2 -0
  239. package/dist/test_search_graph_quality.d.ts.map +1 -0
  240. package/dist/test_search_graph_quality.js +493 -0
  241. package/dist/test_search_graph_quality.js.map +1 -0
  242. package/dist/test_service_to_service_auth.d.ts +2 -0
  243. package/dist/test_service_to_service_auth.d.ts.map +1 -0
  244. package/dist/test_service_to_service_auth.js +199 -0
  245. package/dist/test_service_to_service_auth.js.map +1 -0
  246. package/dist/test_session_next_moves_telemetry.d.ts +2 -0
  247. package/dist/test_session_next_moves_telemetry.d.ts.map +1 -0
  248. package/dist/test_session_next_moves_telemetry.js +100 -0
  249. package/dist/test_session_next_moves_telemetry.js.map +1 -0
  250. package/dist/test_sources_live.d.ts +2 -0
  251. package/dist/test_sources_live.d.ts.map +1 -0
  252. package/dist/test_sources_live.js +30 -0
  253. package/dist/test_sources_live.js.map +1 -0
  254. package/dist/test_specialist_and_analyst_tiers.d.ts +2 -0
  255. package/dist/test_specialist_and_analyst_tiers.d.ts.map +1 -0
  256. package/dist/test_specialist_and_analyst_tiers.js +155 -0
  257. package/dist/test_specialist_and_analyst_tiers.js.map +1 -0
  258. package/dist/test_v2_2_visual_hardening.d.ts +5 -0
  259. package/dist/test_v2_2_visual_hardening.d.ts.map +1 -0
  260. package/dist/test_v2_2_visual_hardening.js +333 -0
  261. package/dist/test_v2_2_visual_hardening.js.map +1 -0
  262. package/dist/test_verify_deprecate_render_spec.d.ts +2 -0
  263. package/dist/test_verify_deprecate_render_spec.d.ts.map +1 -0
  264. package/dist/test_verify_deprecate_render_spec.js +217 -0
  265. package/dist/test_verify_deprecate_render_spec.js.map +1 -0
  266. package/dist/test_verify_neil_demo.d.ts +2 -0
  267. package/dist/test_verify_neil_demo.d.ts.map +1 -0
  268. package/dist/test_verify_neil_demo.js +149 -0
  269. package/dist/test_verify_neil_demo.js.map +1 -0
  270. package/dist/test_waitable_research_and_grounded_links.d.ts +2 -0
  271. package/dist/test_waitable_research_and_grounded_links.d.ts.map +1 -0
  272. package/dist/test_waitable_research_and_grounded_links.js +129 -0
  273. package/dist/test_waitable_research_and_grounded_links.js.map +1 -0
  274. package/dist/test_widget_guards.d.ts +2 -0
  275. package/dist/test_widget_guards.d.ts.map +1 -0
  276. package/dist/test_widget_guards.js +229 -0
  277. package/dist/test_widget_guards.js.map +1 -0
  278. package/dist/test_zero_retention_alerts.d.ts +14 -0
  279. package/dist/test_zero_retention_alerts.d.ts.map +1 -0
  280. package/dist/test_zero_retention_alerts.js +76 -0
  281. package/dist/test_zero_retention_alerts.js.map +1 -0
  282. package/dist/toolHandlers.d.ts +38 -1
  283. package/dist/toolHandlers.d.ts.map +1 -1
  284. package/dist/toolHandlers.js +4290 -711
  285. package/dist/toolHandlers.js.map +1 -1
  286. package/dist/tools.d.ts +22 -1
  287. package/dist/tools.d.ts.map +1 -1
  288. package/dist/tools.js +29 -4
  289. package/dist/tools.js.map +1 -1
  290. package/dist/verify_tools_endpoint.js +19 -5
  291. package/dist/verify_tools_endpoint.js.map +1 -1
  292. package/dist/widgetShell.d.ts +2 -2
  293. package/dist/widgetShell.d.ts.map +1 -1
  294. package/dist/widgetShell.js +1 -1
  295. package/dist/widgetShell.js.map +1 -1
  296. package/package.json +1 -1
  297. package/server.json +3 -31
  298. package/dist/debug_tools.d.ts +0 -2
  299. package/dist/debug_tools.d.ts.map +0 -1
  300. package/dist/debug_tools.js +0 -19
  301. package/dist/debug_tools.js.map +0 -1
  302. package/dist/inspect_list_graphs.d.ts +0 -2
  303. package/dist/inspect_list_graphs.d.ts.map +0 -1
  304. package/dist/inspect_list_graphs.js +0 -25
  305. package/dist/inspect_list_graphs.js.map +0 -1
  306. package/dist/scratch_test.d.ts.map +0 -1
  307. package/dist/scripts/simulate_gemini.d.ts +0 -2
  308. package/dist/scripts/simulate_gemini.d.ts.map +0 -1
  309. package/dist/scripts/simulate_gemini.js +0 -100
  310. package/dist/scripts/simulate_gemini.js.map +0 -1
  311. package/dist/scripts/verify_registry.d.ts +0 -2
  312. package/dist/scripts/verify_registry.d.ts.map +0 -1
  313. package/dist/scripts/verify_registry.js +0 -77
  314. package/dist/scripts/verify_registry.js.map +0 -1
  315. package/dist/src/index.d.ts +0 -2
  316. package/dist/src/index.d.ts.map +0 -1
  317. package/dist/src/index.js +0 -332
  318. package/dist/src/index.js.map +0 -1
  319. package/dist/src/tools.d.ts +0 -12
  320. package/dist/src/tools.d.ts.map +0 -1
  321. package/dist/src/tools.js +0 -104
  322. package/dist/src/tools.js.map +0 -1
  323. package/dist/src/types.d.ts +0 -44
  324. package/dist/src/types.d.ts.map +0 -1
  325. package/dist/src/types.js +0 -2
  326. package/dist/src/types.js.map +0 -1
  327. package/dist/src/verify.d.ts +0 -2
  328. package/dist/src/verify.d.ts.map +0 -1
  329. package/dist/src/verify.js +0 -47
  330. package/dist/src/verify.js.map +0 -1
  331. package/dist/src/verify_hmac.d.ts +0 -2
  332. package/dist/src/verify_hmac.d.ts.map +0 -1
  333. package/dist/src/verify_hmac.js +0 -106
  334. package/dist/src/verify_hmac.js.map +0 -1
  335. package/dist/src/verify_tools_endpoint.d.ts +0 -2
  336. package/dist/src/verify_tools_endpoint.d.ts.map +0 -1
  337. package/dist/src/verify_tools_endpoint.js +0 -40
  338. package/dist/src/verify_tools_endpoint.js.map +0 -1
  339. package/dist/test-waverunner-response.d.ts +0 -2
  340. package/dist/test-waverunner-response.d.ts.map +0 -1
  341. package/dist/test-waverunner-response.js +0 -20
  342. package/dist/test-waverunner-response.js.map +0 -1
  343. package/dist/test_coverage_r2.d.ts +0 -2
  344. package/dist/test_coverage_r2.d.ts.map +0 -1
  345. package/dist/test_coverage_scratch.d.ts +0 -2
  346. 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
- // OAuth discovery endpoints — Clerk-backed OAuth for the MCP Connectors Directory.
48
- // The authorization server is Clerk (configured as CLERK_ISSUER_URL).
49
- // These endpoints follow RFC 8414 / draft-ietf-oauth-security-topics so MCP clients
50
- // can auto-discover the auth server and initiate PKCE flows.
51
- const CLERK_ISSUER = process.env.CLERK_ISSUER_URL || 'https://clerk.fodda.ai';
52
- app.get('/.well-known/oauth-protected-resource', (_req, res) => {
53
- res.status(200).json({
54
- resource: 'https://mcp.fodda.ai',
55
- authorization_servers: [CLERK_ISSUER],
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
- app.get('/.well-known/oauth-protected-resource/mcp', (_req, res) => {
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: 'https://mcp.fodda.ai/mcp',
203
+ resource: resourceUrl,
61
204
  authorization_servers: [CLERK_ISSUER],
62
205
  });
63
206
  });
64
- app.get('/.well-known/oauth-authorization-server', (_req, res) => {
65
- res.redirect(302, `${CLERK_ISSUER}/.well-known/oauth-authorization-server`);
66
- });
67
- app.post('/register', (_req, res) => {
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: 'Canonical token costs defined in API metering.ts ($0.50 via SPT / API credits)',
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: 'Canonical token costs defined in API metering.ts ($0.50 via SPT / API credits)',
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
- async function foddaRequest(method, path, apiKey, userId, body, requestId, source, spt) {
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 60s for analyst consult, and 35s for supplemental.
383
- const AXIOS_TIMEOUT_MS = /\/analysts\/consult/.test(path)
384
- ? 60000
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.status(401).json({
879
- jsonrpc: '2.0',
880
- error: { code: -32000, message: 'OAuth token could not be resolved to a Fodda account. Visit https://app.fodda.ai to reconnect.' },
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.status(401).json({
895
- jsonrpc: '2.0',
896
- error: { code: -32000, message: 'OAuth token validation failed. Please reconnect at https://app.fodda.ai.' },
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.status(401).json({ error: 'Invalid or expired connection token' });
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 ? (req.headers['authorization']?.toString().replace(/^Bearer\s+/i, '')) : '')
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
- // Priority for userId: resolved OAuth user → resolved token email → query user_id → header X-User-Id → query id / anonymous
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 defaultSource = (offeringSlug !== 'mcp' && allowedTools !== undefined) ? offeringSlug : (isSpt ? 'spt' : '');
937
- const source = req.query.source || defaultSource;
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: /mcp requires authentication — Clerk OAuth,
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 (offeringSlug === 'mcp' && !isSpt && !apiKey && process.env.MCP_ALLOW_ANONYMOUS !== 'true') {
949
- res.setHeader('WWW-Authenticate', 'Bearer resource_metadata="https://mcp.fodda.ai/.well-known/oauth-protected-resource/mcp"');
950
- return res.status(401).json({
951
- jsonrpc: '2.0',
952
- error: { code: -32000, message: 'Authentication required. Connect via OAuth, or use your personal connection URL or API key from https://app.fodda.ai.' },
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
- ? ((m, p, k, u, b, r) => foddaRequest(m, p, k, u, b, r, source))
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
- return res.status(400).json({
1020
- jsonrpc: '2.0',
1021
- error: { code: -32000, message: 'Bad Request: Session required' },
1022
- id: null
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
- // Inject Accept: text/event-stream if missing (prevents SDK 406)
1034
- const accept = req.headers['accept'] || '';
1035
- if (!accept.includes('text/event-stream')) {
1036
- req.headers['accept'] = accept ? `${accept}, text/event-stream` : 'application/json, text/event-stream';
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';