@@ -339,6 +339,375 @@ export async function evaluate(options: EvaluateOptions): Promise<EvaluateResult
339339 } ;
340340}
341341
342+ // ---------------------------------------------------------------------------
343+ // MCP: JSON-RPC over InvokeAgentRuntime
344+ // ---------------------------------------------------------------------------
345+
346+ export interface McpInvokeOptions {
347+ region : string ;
348+ runtimeArn : string ;
349+ userId ?: string ;
350+ mcpSessionId ?: string ;
351+ logger ?: SSELogger ;
352+ }
353+
354+ export interface McpToolDef {
355+ name : string ;
356+ description ?: string ;
357+ inputSchema ?: Record < string , unknown > ;
358+ }
359+
360+ export interface McpListToolsResult {
361+ tools : McpToolDef [ ] ;
362+ mcpSessionId ?: string ;
363+ }
364+
365+ let mcpRequestId = 1 ;
366+
367+ interface McpRpcResult {
368+ result : Record < string , unknown > ;
369+ mcpSessionId ?: string ;
370+ error ?: { message ?: string ; code ?: number } ;
371+ }
372+
373+ /** Send a JSON-RPC payload through InvokeAgentRuntime and return the parsed response. */
374+ async function mcpRpcCall ( options : McpInvokeOptions , body : Record < string , unknown > ) : Promise < McpRpcResult > {
375+ const client = new BedrockAgentCoreClient ( {
376+ region : options . region ,
377+ credentials : getCredentialProvider ( ) ,
378+ } ) ;
379+
380+ options . logger ?. logSSEEvent ( `MCP request: ${ JSON . stringify ( body ) } ` ) ;
381+
382+ const command = new InvokeAgentRuntimeCommand ( {
383+ agentRuntimeArn : options . runtimeArn ,
384+ payload : new TextEncoder ( ) . encode ( JSON . stringify ( body ) ) ,
385+ contentType : 'application/json' ,
386+ accept : 'application/json, text/event-stream' ,
387+ mcpSessionId : options . mcpSessionId ,
388+ mcpProtocolVersion : '2025-03-26' ,
389+ runtimeUserId : options . userId ?? DEFAULT_RUNTIME_USER_ID ,
390+ } ) ;
391+
392+ const response = await client . send ( command ) ;
393+
394+ if ( ! response . response ) {
395+ throw new Error ( 'No response from AgentCore Runtime' ) ;
396+ }
397+
398+ const bytes = await response . response . transformToByteArray ( ) ;
399+ const text = new TextDecoder ( ) . decode ( bytes ) ;
400+
401+ options . logger ?. logSSEEvent ( `MCP response: ${ text } ` ) ;
402+
403+ const parsed = parseJsonRpcResponse ( text ) ;
404+
405+ return {
406+ result : ( parsed . result as Record < string , unknown > ) ?? { } ,
407+ mcpSessionId : response . mcpSessionId ,
408+ error : parsed . error as McpRpcResult [ 'error' ] ,
409+ } ;
410+ }
411+
412+ /** Call mcpRpcCall and throw on JSON-RPC errors. Use mcpRpcCall directly when errors should be tolerated. */
413+ async function mcpRpcCallStrict ( options : McpInvokeOptions , body : Record < string , unknown > ) : Promise < McpRpcResult > {
414+ const result = await mcpRpcCall ( options , body ) ;
415+ if ( result . error ) {
416+ throw new Error ( result . error . message ?? `MCP error (code ${ result . error . code } )` ) ;
417+ }
418+ return result ;
419+ }
420+
421+ /** Send a JSON-RPC notification (no id, no response expected). */
422+ async function mcpRpcNotify ( options : McpInvokeOptions , body : Record < string , unknown > ) : Promise < void > {
423+ const client = new BedrockAgentCoreClient ( {
424+ region : options . region ,
425+ credentials : getCredentialProvider ( ) ,
426+ } ) ;
427+
428+ const command = new InvokeAgentRuntimeCommand ( {
429+ agentRuntimeArn : options . runtimeArn ,
430+ payload : new TextEncoder ( ) . encode ( JSON . stringify ( body ) ) ,
431+ contentType : 'application/json' ,
432+ accept : 'application/json, text/event-stream' ,
433+ mcpSessionId : options . mcpSessionId ,
434+ mcpProtocolVersion : '2025-03-26' ,
435+ runtimeUserId : options . userId ?? DEFAULT_RUNTIME_USER_ID ,
436+ } ) ;
437+
438+ await client . send ( command ) ;
439+ }
440+
441+ /**
442+ * Initialize MCP session and list available tools via InvokeAgentRuntime.
443+ * Retries on cold-start initialization timeouts.
444+ */
445+ export async function mcpListTools ( options : McpInvokeOptions ) : Promise < McpListToolsResult > {
446+ const maxRetries = 3 ;
447+
448+ for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
449+ try {
450+ return await mcpListToolsOnce ( options ) ;
451+ } catch ( err ) {
452+ const msg = err instanceof Error ? err . message : String ( err ) ;
453+ const isColdStart = msg . includes ( 'initialization time exceeded' ) || msg . includes ( 'initialization' ) ;
454+
455+ if ( isColdStart && attempt < maxRetries - 1 ) {
456+ options . logger ?. logSSEEvent ( `MCP cold start (attempt ${ attempt + 1 } /${ maxRetries } ), retrying...` ) ;
457+ await new Promise ( resolve => setTimeout ( resolve , 2000 ) ) ;
458+ continue ;
459+ }
460+ throw err ;
461+ }
462+ }
463+
464+ throw new Error ( 'Failed to list MCP tools after retries' ) ;
465+ }
466+
467+ async function mcpListToolsOnce ( options : McpInvokeOptions ) : Promise < McpListToolsResult > {
468+ // 1. Initialize — tolerate JSON-RPC errors (stateless servers may reject initialize but still return a session ID)
469+ const initResult = await mcpRpcCall ( options , {
470+ jsonrpc : '2.0' ,
471+ id : mcpRequestId ++ ,
472+ method : 'initialize' ,
473+ params : {
474+ protocolVersion : '2025-03-26' ,
475+ capabilities : { } ,
476+ clientInfo : { name : 'agentcore-cli' , version : '1.0.0' } ,
477+ } ,
478+ } ) ;
479+
480+ if ( initResult . error ) {
481+ options . logger ?. logSSEEvent (
482+ `MCP initialize returned error (expected for stateless servers): ${ initResult . error . message } `
483+ ) ;
484+ }
485+
486+ const sessionId = initResult . mcpSessionId ;
487+ const optionsWithSession = { ...options , mcpSessionId : sessionId } ;
488+
489+ // 2. Send initialized notification
490+ await mcpRpcNotify ( optionsWithSession , {
491+ jsonrpc : '2.0' ,
492+ method : 'notifications/initialized' ,
493+ } ) ;
494+
495+ // 3. List tools
496+ const listResult = await mcpRpcCallStrict ( optionsWithSession , {
497+ jsonrpc : '2.0' ,
498+ id : mcpRequestId ++ ,
499+ method : 'tools/list' ,
500+ params : { } ,
501+ } ) ;
502+
503+ const tools = ( listResult . result as { tools ?: McpToolDef [ ] } ) . tools ?? [ ] ;
504+
505+ return {
506+ tools : tools . map ( t => ( { name : t . name , description : t . description , inputSchema : t . inputSchema } ) ) ,
507+ mcpSessionId : sessionId ,
508+ } ;
509+ }
510+
511+ /**
512+ * Initialize an MCP session (without listing tools).
513+ * Returns just the session ID needed for subsequent tool calls.
514+ */
515+ export async function mcpInitSession ( options : McpInvokeOptions ) : Promise < string | undefined > {
516+ const initResult = await mcpRpcCall ( options , {
517+ jsonrpc : '2.0' ,
518+ id : mcpRequestId ++ ,
519+ method : 'initialize' ,
520+ params : {
521+ protocolVersion : '2025-03-26' ,
522+ capabilities : { } ,
523+ clientInfo : { name : 'agentcore-cli' , version : '1.0.0' } ,
524+ } ,
525+ } ) ;
526+
527+ const sessionId = initResult . mcpSessionId ;
528+ const optionsWithSession = { ...options , mcpSessionId : sessionId } ;
529+
530+ await mcpRpcNotify ( optionsWithSession , {
531+ jsonrpc : '2.0' ,
532+ method : 'notifications/initialized' ,
533+ } ) ;
534+
535+ return sessionId ;
536+ }
537+
538+ /**
539+ * Call an MCP tool via InvokeAgentRuntime.
540+ * Retries on cold-start initialization timeouts.
541+ */
542+ export async function mcpCallTool (
543+ options : McpInvokeOptions ,
544+ toolName : string ,
545+ args : Record < string , unknown >
546+ ) : Promise < string > {
547+ const maxRetries = 3 ;
548+
549+ for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
550+ try {
551+ const { result } = await mcpRpcCallStrict ( options , {
552+ jsonrpc : '2.0' ,
553+ id : mcpRequestId ++ ,
554+ method : 'tools/call' ,
555+ params : { name : toolName , arguments : args } ,
556+ } ) ;
557+
558+ const content = ( result as { content ?: { type ?: string ; text ?: string } [ ] } ) . content ;
559+ if ( content ) {
560+ const texts : string [ ] = [ ] ;
561+ for ( const item of content ) {
562+ if ( item . text !== undefined ) {
563+ texts . push ( item . text ) ;
564+ }
565+ }
566+ if ( texts . length > 0 ) return texts . join ( '' ) ;
567+ }
568+
569+ return JSON . stringify ( result , null , 2 ) ;
570+ } catch ( err ) {
571+ const msg = err instanceof Error ? err . message : String ( err ) ;
572+ const isColdStart = msg . includes ( 'initialization time exceeded' ) || msg . includes ( 'initialization' ) ;
573+
574+ if ( isColdStart && attempt < maxRetries - 1 ) {
575+ options . logger ?. logSSEEvent ( `MCP cold start (attempt ${ attempt + 1 } /${ maxRetries } ), retrying...` ) ;
576+ await new Promise ( resolve => setTimeout ( resolve , 2000 ) ) ;
577+ continue ;
578+ }
579+ throw err ;
580+ }
581+ }
582+
583+ throw new Error ( 'Failed to call MCP tool after retries' ) ;
584+ }
585+
586+ // ---------------------------------------------------------------------------
587+ // A2A: JSON-RPC message/send over InvokeAgentRuntime
588+ // ---------------------------------------------------------------------------
589+
590+ export interface A2AInvokeOptions {
591+ region : string ;
592+ runtimeArn : string ;
593+ userId ?: string ;
594+ logger ?: SSELogger ;
595+ }
596+
597+ let a2aRequestId = 1 ;
598+
599+ /**
600+ * Invoke a deployed A2A agent via InvokeAgentRuntime with JSON-RPC message/send.
601+ * Streams text parts from the response artifacts.
602+ */
603+ export async function invokeA2ARuntime ( options : A2AInvokeOptions , message : string ) : Promise < StreamingInvokeResult > {
604+ const client = new BedrockAgentCoreClient ( {
605+ region : options . region ,
606+ credentials : getCredentialProvider ( ) ,
607+ } ) ;
608+
609+ const body = {
610+ jsonrpc : '2.0' ,
611+ id : a2aRequestId ++ ,
612+ method : 'message/send' ,
613+ params : {
614+ message : {
615+ role : 'user' ,
616+ parts : [ { kind : 'text' , text : message } ] ,
617+ messageId : `msg-${ Date . now ( ) } ` ,
618+ } ,
619+ } ,
620+ } ;
621+
622+ options . logger ?. logSSEEvent ( `A2A request: ${ JSON . stringify ( body ) } ` ) ;
623+
624+ const command = new InvokeAgentRuntimeCommand ( {
625+ agentRuntimeArn : options . runtimeArn ,
626+ payload : new TextEncoder ( ) . encode ( JSON . stringify ( body ) ) ,
627+ contentType : 'application/json' ,
628+ accept : 'application/json, text/event-stream' ,
629+ runtimeUserId : options . userId ?? DEFAULT_RUNTIME_USER_ID ,
630+ } ) ;
631+
632+ const response = await client . send ( command ) ;
633+
634+ if ( ! response . response ) {
635+ throw new Error ( 'No response from AgentCore Runtime' ) ;
636+ }
637+
638+ const bytes = await response . response . transformToByteArray ( ) ;
639+ const text = new TextDecoder ( ) . decode ( bytes ) ;
640+
641+ options . logger ?. logSSEEvent ( `A2A response: ${ text } ` ) ;
642+
643+ const parsed = parseA2AResponse ( text ) ;
644+
645+ return {
646+ stream : singleValueStream ( parsed ) ,
647+ sessionId : undefined ,
648+ } ;
649+ }
650+
651+ /** Wrap a single string value as an AsyncGenerator for StreamingInvokeResult compatibility. */
652+ async function * singleValueStream ( value : string ) : AsyncGenerator < string , void , unknown > {
653+ yield await Promise . resolve ( value ) ;
654+ }
655+
656+ /** Extract text content from A2A JSON-RPC response. Supports both kind:'text' and type:'text' part formats. */
657+ export function parseA2AResponse ( text : string ) : string {
658+ try {
659+ const parsed : unknown = JSON . parse ( text ) ;
660+ if ( ! parsed || typeof parsed !== 'object' ) return text ;
661+
662+ const obj = parsed as Record < string , unknown > ;
663+
664+ // Check for JSON-RPC error
665+ if ( obj . error && typeof obj . error === 'object' ) {
666+ const err = obj . error as { message ?: string } ;
667+ return `Error: ${ err . message ?? JSON . stringify ( obj . error ) } ` ;
668+ }
669+
670+ // Extract text from result.artifacts[].parts[].text
671+ const result = obj . result as Record < string , unknown > | undefined ;
672+ if ( ! result ) return text ;
673+
674+ const artifacts = result . artifacts as { parts ?: { kind ?: string ; type ?: string ; text ?: string } [ ] } [ ] | undefined ;
675+ if ( artifacts ) {
676+ const texts : string [ ] = [ ] ;
677+ for ( const artifact of artifacts ) {
678+ if ( artifact . parts ) {
679+ for ( const part of artifact . parts ) {
680+ if ( ( part . kind === 'text' || part . type === 'text' ) && part . text !== undefined ) {
681+ texts . push ( part . text ) ;
682+ }
683+ }
684+ }
685+ }
686+ if ( texts . length > 0 ) return texts . join ( '' ) ;
687+ }
688+
689+ // Fallback: check history for the last assistant message
690+ const history = result . history as
691+ | { role ?: string ; parts ?: { kind ?: string ; type ?: string ; text ?: string } [ ] } [ ]
692+ | undefined ;
693+ if ( history ) {
694+ for ( let i = history . length - 1 ; i >= 0 ; i -- ) {
695+ const msg = history [ i ] ;
696+ if ( msg ?. role === 'agent' && msg . parts ) {
697+ const agentTexts = msg . parts
698+ . filter ( p => ( p . kind === 'text' || p . type === 'text' ) && p . text !== undefined )
699+ . map ( p => p . text ! ) ;
700+ if ( agentTexts . length > 0 ) return agentTexts . join ( '' ) ;
701+ }
702+ }
703+ }
704+
705+ return JSON . stringify ( result , null , 2 ) ;
706+ } catch {
707+ return text ;
708+ }
709+ }
710+
342711/**
343712 * Stop a runtime session.
344713 */
0 commit comments