@@ -2694,6 +2694,176 @@ describe('api/mcp.ts — U7 Pro-path', () => {
26942694 assert . match ( sig , / ^ \d { 10 } \. [ A - Z a - z 0 - 9 _ - ] + $ / , 'signature must be <ts>.<base64url-sig>' ) ;
26952695 } ) ;
26962696
2697+ it ( 'edge: Pro get_country_brief signs a short URL and sends grounding context in the POST body' , async ( ) => {
2698+ const { deps } = makeProDeps ( ) ;
2699+ const captured = [ ] ;
2700+ globalThis . fetch = async ( url , init = { } ) => {
2701+ const call = {
2702+ url : String ( url ) ,
2703+ method : init . method || 'GET' ,
2704+ headers : new Headers ( init . headers ) ,
2705+ body : typeof init . body === 'string' ? init . body : '' ,
2706+ } ;
2707+ captured . push ( call ) ;
2708+ const { pathname } = new URL ( call . url ) ;
2709+
2710+ if ( pathname === '/api/news/v1/list-feed-digest' ) {
2711+ const items = Array . from ( { length : 15 } , ( _ , index ) => ( {
2712+ title : `Iran ${ index } ${ '%' . repeat ( 300 ) } ` ,
2713+ source : 'Context Wire' ,
2714+ link : `https://example.com/iran-${ index } ` ,
2715+ publishedAt : '2026-06-07T00:00:00.000Z' ,
2716+ snippet : 'Long Iran grounding item used to keep the MCP signed URL below proxy limits.' ,
2717+ } ) ) ;
2718+ return new Response ( JSON . stringify ( { categories : { world : { items } } } ) , {
2719+ status : 200 ,
2720+ headers : { 'Content-Type' : 'application/json' } ,
2721+ } ) ;
2722+ }
2723+
2724+ if ( pathname === '/api/intelligence/v1/get-country-intel-brief' ) {
2725+ return new Response ( JSON . stringify ( { brief : 'Grounded country brief.' } ) , {
2726+ status : 200 ,
2727+ headers : { 'Content-Type' : 'application/json' } ,
2728+ } ) ;
2729+ }
2730+
2731+ throw new Error ( `Unexpected fetch URL: ${ call . url } ` ) ;
2732+ } ;
2733+
2734+ const res = await mcpHandler ( proReq ( 'POST' , callBody ( 'get_country_brief' , {
2735+ country_code : 'IR' ,
2736+ framework : 'PMESII-PT' ,
2737+ } ) ) , deps ) ;
2738+ assert . equal ( res . status , 200 ) ;
2739+ const rpc = await res . json ( ) ;
2740+ assert . ok ( rpc . result ?. content , 'tool call must return content' ) ;
2741+
2742+ const countryCall = captured . find ( ( call ) => new URL ( call . url ) . pathname === '/api/intelligence/v1/get-country-intel-brief' ) ;
2743+ assert . ok ( countryCall , 'country brief fetch must run' ) ;
2744+ const countryUrl = new URL ( countryCall . url ) ;
2745+ assert . equal ( countryUrl . searchParams . has ( 'context' ) , false , 'context must not be signed in the URL query' ) ;
2746+ assert . ok ( countryCall . url . length < 200 , `signed URL should stay short, got ${ countryCall . url . length } chars` ) ;
2747+
2748+ const body = JSON . parse ( countryCall . body ) ;
2749+ assert . equal ( body . country_code , 'IR' ) ;
2750+ assert . equal ( body . framework , 'PMESII-PT' ) ;
2751+ assert . match ( body . context , / B r i e f s o u r c e a r t i c l e s : / ) ;
2752+ assert . match ( body . context , / H e a d l i n e s : / ) ;
2753+ assert . match ( body . context , / I r a n / ) ;
2754+ assert . ok ( body . context . length > 1000 , `expected large grounding context, got ${ body . context . length } chars` ) ;
2755+ assert . ok ( body . context . length <= 4000 , `grounding context should be bounded to 4000 chars, got ${ body . context . length } ` ) ;
2756+
2757+ assert . ok ( countryCall . headers . get ( 'x-wm-mcp-internal' ) , 'X-WM-MCP-Internal must be set' ) ;
2758+ assert . equal ( countryCall . headers . get ( 'x-wm-mcp-user-id' ) , PRO_USER_ID ) ;
2759+ assert . equal ( countryCall . headers . get ( 'x-worldmonitor-key' ) , null , 'X-WorldMonitor-Key must NOT be set for Pro' ) ;
2760+
2761+ const { verifyInternalMcpRequest } = await import ( `../server/_shared/mcp-internal-hmac.ts?t=${ Date . now ( ) } ` ) ;
2762+ const signedReq = new Request ( countryCall . url , {
2763+ method : 'POST' ,
2764+ headers : countryCall . headers ,
2765+ body : countryCall . body ,
2766+ } ) ;
2767+ assert . ok ( await verifyInternalMcpRequest ( signedReq , HMAC_SECRET ) , 'signature must verify for the short URL plus context body' ) ;
2768+
2769+ const tamperedUrl = `${ countryCall . url } ?context=${ encodeURIComponent ( body . context ) } ` ;
2770+ const tamperedReq = new Request ( tamperedUrl , {
2771+ method : 'POST' ,
2772+ headers : countryCall . headers ,
2773+ body : countryCall . body ,
2774+ } ) ;
2775+ assert . equal ( await verifyInternalMcpRequest ( tamperedReq , HMAC_SECRET ) , null , 'same signature must not verify if context is moved back into the URL' ) ;
2776+ } ) ;
2777+
2778+ it ( 'edge: Pro get_country_brief surfaces gateway error detail for Sentry grouping' , async ( ) => {
2779+ const scenarios = [
2780+ {
2781+ name : 'structured HMAC 401' ,
2782+ expectedLog : 'warn' ,
2783+ makeResponse : ( ) => new Response (
2784+ JSON . stringify ( { error : 'invalid_internal_mcp_signature' } ) ,
2785+ { status : 401 , headers : { 'Content-Type' : 'application/json' } } ,
2786+ ) ,
2787+ assertMessage : ( message ) => {
2788+ assert . equal ( message , 'get-country-intel-brief HTTP 401: invalid_internal_mcp_signature' ) ;
2789+ } ,
2790+ } ,
2791+ {
2792+ name : 'HTML CDN 503' ,
2793+ expectedLog : 'error' ,
2794+ makeResponse : ( ) => new Response (
2795+ '<!DOCTYPE html><html><head><title>Bad Gateway</title></head><body>Cloudflare outage</body></html>' ,
2796+ { status : 503 , headers : { 'Content-Type' : 'text/html' } } ,
2797+ ) ,
2798+ assertMessage : ( message ) => {
2799+ assert . equal ( message , 'get-country-intel-brief HTTP 503: Bad Gateway Cloudflare outage' ) ;
2800+ assert . doesNotMatch ( message , / < [ ^ > ] * > / , 'HTML tags must not leak into the Sentry/log title' ) ;
2801+ } ,
2802+ } ,
2803+ {
2804+ name : 'unreadable body 503' ,
2805+ expectedLog : 'error' ,
2806+ makeResponse : ( ) => ( { ok : false , status : 503 , text : async ( ) => { throw new Error ( 'body locked' ) ; } } ) ,
2807+ assertMessage : ( message ) => {
2808+ assert . equal ( message , 'get-country-intel-brief HTTP 503' ) ;
2809+ } ,
2810+ } ,
2811+ ] ;
2812+
2813+ for ( const scenario of scenarios ) {
2814+ const { deps } = makeProDeps ( ) ;
2815+ const warnCalls = [ ] ;
2816+ const errorCalls = [ ] ;
2817+ const origWarn = console . warn ;
2818+ const origError = console . error ;
2819+ console . warn = ( ...args ) => { warnCalls . push ( args ) ; } ;
2820+ console . error = ( ...args ) => { errorCalls . push ( args ) ; } ;
2821+ try {
2822+ globalThis . fetch = async ( url ) => {
2823+ const { pathname } = new URL ( String ( url ) ) ;
2824+ if ( pathname === '/api/news/v1/list-feed-digest' ) {
2825+ return new Response ( JSON . stringify ( {
2826+ categories : {
2827+ world : {
2828+ items : [ {
2829+ title : 'Iran headline for failing country brief' ,
2830+ source : 'Context Wire' ,
2831+ link : 'https://example.com/iran-failure' ,
2832+ publishedAt : '2026-06-07T00:00:00.000Z' ,
2833+ snippet : 'Iran context item used before the brief endpoint fails.' ,
2834+ } ] ,
2835+ } ,
2836+ } ,
2837+ } ) , { status : 200 , headers : { 'Content-Type' : 'application/json' } } ) ;
2838+ }
2839+ if ( pathname === '/api/intelligence/v1/get-country-intel-brief' ) {
2840+ return scenario . makeResponse ( ) ;
2841+ }
2842+ return new Response ( '' , { status : 200 } ) ;
2843+ } ;
2844+
2845+ const res = await mcpHandler ( proReq ( 'POST' , callBody ( 'get_country_brief' , {
2846+ country_code : 'IR' ,
2847+ framework : 'PMESII-PT' ,
2848+ } ) ) , deps ) ;
2849+ assert . equal ( res . status , 200 , `${ scenario . name } : JSON-RPC tool errors stay HTTP 200` ) ;
2850+ const rpc = await res . json ( ) ;
2851+ assert . equal ( rpc . error ?. code , - 32603 , `${ scenario . name } : tool failure should be a JSON-RPC internal error` ) ;
2852+
2853+ const expectedCalls = scenario . expectedLog === 'warn' ? warnCalls : errorCalls ;
2854+ const unexpectedCalls = scenario . expectedLog === 'warn' ? errorCalls : warnCalls ;
2855+ const mcpLogs = expectedCalls . filter ( ( args ) => args [ 0 ] === '[mcp] tool execution error:' ) ;
2856+ assert . equal ( mcpLogs . length , 1 , `${ scenario . name } : expected one MCP execution log` ) ;
2857+ assert . equal ( unexpectedCalls . some ( ( args ) => args [ 0 ] === '[mcp] tool execution error:' ) , false , `${ scenario . name } : wrong console severity` ) ;
2858+ const loggedError = mcpLogs [ 0 ] [ 1 ] ;
2859+ scenario . assertMessage ( loggedError instanceof Error ? loggedError . message : String ( loggedError ) ) ;
2860+ } finally {
2861+ console . warn = origWarn ;
2862+ console . error = origError ;
2863+ }
2864+ }
2865+ } ) ;
2866+
26972867 it ( 'edge: cache-only tool for Pro user goes through INCR/DECR path (counts toward 50/day)' , async ( ) => {
26982868 const { deps, pipe } = makeProDeps ( ) ;
26992869 process . env . UPSTASH_REDIS_REST_URL = 'https://stub.upstash' ;
0 commit comments