@@ -544,6 +544,202 @@ test('callToolActivity awaits tool.execute before closing MCP client', async (t)
544544 ) ;
545545} ) ;
546546
547+ test ( 'MCP tool inputSchema survives activity serialization' , async ( t ) => {
548+ // This test verifies that tool inputSchema is correctly reconstructed after
549+ // being serialized through Temporal's activity boundary.
550+ //
551+ // The bug: When the listTools activity returns, v.inputSchema is a schema wrapper
552+ // object with a getter. After JSON serialization (Temporal activity return), it becomes
553+ // { jsonSchema: {...} }. The workflow code then wraps this again with jsonSchema(),
554+ // creating { jsonSchema: { jsonSchema: {...} } } - double nesting that breaks the schema.
555+ //
556+ // This caused:
557+ // - "Invalid schema: type 'None'" errors from OpenAI (type was undefined)
558+ // - Missing properties in tool definitions (#1889)
559+ // - Missing property descriptions (#1889)
560+
561+ const { jsonSchema } = await import ( 'ai' ) ;
562+
563+ // Simulate what the AI SDK's MCP client returns for a tool's inputSchema
564+ // Include property descriptions to test #1889 fix
565+ const originalSchema = jsonSchema ( {
566+ type : 'object' ,
567+ properties : {
568+ regionId : {
569+ type : 'string' ,
570+ description : 'The region ID to query' ,
571+ } ,
572+ limit : {
573+ type : 'number' ,
574+ description : 'Maximum results to return' ,
575+ } ,
576+ } ,
577+ required : [ 'regionId' ] ,
578+ additionalProperties : false ,
579+ } ) ;
580+
581+ // Simulate what the listTools activity returns (extracts inputSchema from tool)
582+ const activityResult = {
583+ testTool : {
584+ description : 'A test tool' ,
585+ inputSchema : originalSchema ,
586+ } ,
587+ } ;
588+
589+ // Simulate JSON serialization that happens when activity returns to workflow
590+ // This is what Temporal does when passing data from activity to workflow
591+ const serialized = JSON . stringify ( activityResult ) ;
592+ const deserialized = JSON . parse ( serialized ) ;
593+
594+ // This is what the workflow receives - inputSchema is now { jsonSchema: {...} }
595+ const toolResult = deserialized . testTool ;
596+
597+ // Verify the serialized structure
598+ t . truthy ( toolResult . inputSchema . jsonSchema , 'After serialization, inputSchema has jsonSchema property' ) ;
599+ t . is ( toolResult . inputSchema . type , undefined , 'After serialization, inputSchema.type is undefined (nested)' ) ;
600+
601+ // The fix: access toolResult.inputSchema.jsonSchema before wrapping
602+ const fixedSchema = jsonSchema ( toolResult . inputSchema . jsonSchema ) ;
603+
604+ // Verify schema type is preserved
605+ t . is ( fixedSchema . jsonSchema . type , 'object' , 'Schema type should be "object"' ) ;
606+
607+ // Verify properties are preserved (#1889)
608+ t . truthy ( fixedSchema . jsonSchema . properties , 'Schema should have properties' ) ;
609+ t . truthy ( fixedSchema . jsonSchema . properties . regionId , 'Schema should have regionId property' ) ;
610+ t . truthy ( fixedSchema . jsonSchema . properties . limit , 'Schema should have limit property' ) ;
611+
612+ // Verify property descriptions are preserved (#1889)
613+ t . is (
614+ fixedSchema . jsonSchema . properties . regionId . description ,
615+ 'The region ID to query' ,
616+ 'Property description should be preserved'
617+ ) ;
618+ t . is (
619+ fixedSchema . jsonSchema . properties . limit . description ,
620+ 'Maximum results to return' ,
621+ 'Property description should be preserved'
622+ ) ;
623+
624+ // Verify required fields are preserved
625+ t . deepEqual ( fixedSchema . jsonSchema . required , [ 'regionId' ] , 'Required fields should be preserved' ) ;
626+
627+ // Verify additionalProperties is preserved
628+ t . is ( fixedSchema . jsonSchema . additionalProperties , false , 'additionalProperties should be preserved' ) ;
629+
630+ // Also verify what the buggy code produces (to document the bug)
631+ const buggySchema = jsonSchema ( toolResult . inputSchema ) ;
632+ t . is ( buggySchema . jsonSchema . type , undefined , 'Buggy: type is undefined (double-wrapped)' ) ;
633+ t . is ( buggySchema . jsonSchema . properties , undefined , 'Buggy: properties is undefined (double-wrapped)' ) ;
634+ t . truthy ( buggySchema . jsonSchema . jsonSchema , 'Buggy: has nested jsonSchema showing double-wrapping' ) ;
635+ } ) ;
636+
637+ test ( 'MCP listTools activity preserves tool and parameter metadata (#1889)' , async ( t ) => {
638+ // This test verifies the full flow from MCP client through activity serialization,
639+ // ensuring tool descriptions and parameter metadata are preserved.
640+ // Regression test for https://github.com/temporalio/sdk-typescript/issues/1889
641+
642+ const { jsonSchema } = await import ( 'ai' ) ;
643+
644+ // Simulate a real MCP tool with full metadata
645+ const mockMcpToolSchema = jsonSchema ( {
646+ type : 'object' ,
647+ properties : {
648+ regionId : {
649+ type : 'string' ,
650+ description : 'The AWS region identifier (e.g., us-east-1)' ,
651+ } ,
652+ serviceType : {
653+ type : 'string' ,
654+ enum : [ 'compute' , 'storage' , 'database' ] ,
655+ description : 'Type of service to filter by' ,
656+ } ,
657+ maxResults : {
658+ type : 'integer' ,
659+ description : 'Maximum number of services to return' ,
660+ default : 10 ,
661+ } ,
662+ } ,
663+ required : [ 'regionId' ] ,
664+ additionalProperties : false ,
665+ } ) ;
666+
667+ // Create mock MCP client similar to what @ai -sdk/mcp returns
668+ const mockMcpClient = {
669+ async tools ( ) {
670+ return {
671+ 'list-services' : {
672+ description : 'Lists all available services in a specified AWS region with optional filtering' ,
673+ inputSchema : mockMcpToolSchema ,
674+ execute : async ( ) => ( { services : [ ] } ) ,
675+ } ,
676+ } ;
677+ } ,
678+ async close ( ) { } ,
679+ } ;
680+
681+ // Simulate what listToolsActivity does (from activities.ts)
682+ const mcpTools = await mockMcpClient . tools ( ) ;
683+ const activityResult = Object . fromEntries (
684+ Object . entries ( mcpTools ) . map ( ( [ k , v ] ) => [
685+ k ,
686+ {
687+ description : v . description ,
688+ inputSchema : v . inputSchema ,
689+ } ,
690+ ] )
691+ ) ;
692+
693+ // Simulate Temporal activity serialization round-trip
694+ const serialized = JSON . stringify ( activityResult ) ;
695+ const workflowReceives = JSON . parse ( serialized ) ;
696+
697+ // Verify tool description is preserved
698+ t . is (
699+ workflowReceives [ 'list-services' ] . description ,
700+ 'Lists all available services in a specified AWS region with optional filtering' ,
701+ 'Tool description should survive serialization'
702+ ) ;
703+
704+ // Simulate what mcp.ts does with the fix applied
705+ const toolResult = workflowReceives [ 'list-services' ] ;
706+ const reconstructedSchema = jsonSchema ( toolResult . inputSchema . jsonSchema ) ;
707+
708+ // Verify all schema metadata is preserved
709+ const schema = reconstructedSchema . jsonSchema ;
710+
711+ t . is ( schema . type , 'object' , 'Schema type preserved' ) ;
712+ t . deepEqual ( schema . required , [ 'regionId' ] , 'Required fields preserved' ) ;
713+ t . is ( schema . additionalProperties , false , 'additionalProperties preserved' ) ;
714+
715+ // Verify all properties and their metadata
716+ t . truthy ( schema . properties . regionId , 'regionId property exists' ) ;
717+ t . is ( schema . properties . regionId . type , 'string' , 'regionId type preserved' ) ;
718+ t . is (
719+ schema . properties . regionId . description ,
720+ 'The AWS region identifier (e.g., us-east-1)' ,
721+ 'regionId description preserved'
722+ ) ;
723+
724+ t . truthy ( schema . properties . serviceType , 'serviceType property exists' ) ;
725+ t . is ( schema . properties . serviceType . type , 'string' , 'serviceType type preserved' ) ;
726+ t . deepEqual ( schema . properties . serviceType . enum , [ 'compute' , 'storage' , 'database' ] , 'serviceType enum preserved' ) ;
727+ t . is (
728+ schema . properties . serviceType . description ,
729+ 'Type of service to filter by' ,
730+ 'serviceType description preserved'
731+ ) ;
732+
733+ t . truthy ( schema . properties . maxResults , 'maxResults property exists' ) ;
734+ t . is ( schema . properties . maxResults . type , 'integer' , 'maxResults type preserved' ) ;
735+ t . is ( schema . properties . maxResults . default , 10 , 'maxResults default preserved' ) ;
736+ t . is (
737+ schema . properties . maxResults . description ,
738+ 'Maximum number of services to return' ,
739+ 'maxResults description preserved'
740+ ) ;
741+ } ) ;
742+
547743// Currently fails in CI due to invalid server response but passes locally
548744test . skip ( 'MCP Use' , async ( t ) => {
549745 if ( remoteTests ) {
0 commit comments