diff --git a/packages/cli/package.json b/packages/cli/package.json index 2bf8d83..cd77381 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/cli", - "version": "1.0.12", + "version": "1.1.0", "description": "CLI utilities for UTCP", "main": "dist/index.cjs", "module": "dist/index.js", @@ -43,7 +43,7 @@ } }, "dependencies": { - "@utcp/sdk": "^1.0.15" + "@utcp/sdk": "^1.1.0" }, "devDependencies": { "bun-types": "latest", diff --git a/packages/cli/src/cli_call_template.ts b/packages/cli/src/cli_call_template.ts index aa900a7..8746c79 100644 --- a/packages/cli/src/cli_call_template.ts +++ b/packages/cli/src/cli_call_template.ts @@ -162,6 +162,7 @@ export interface CliCallTemplate extends CallTemplate { env_vars?: Record | null; working_dir?: string | null; auth?: undefined; + allowed_communication_protocols?: string[]; } /** @@ -174,6 +175,7 @@ export const CliCallTemplateSchema: z.ZodType = z.object({ env_vars: z.record(z.string(), z.string()).nullable().optional().describe('Environment variables to set when executing the commands'), working_dir: z.string().nullable().optional().describe('Working directory for command execution'), auth: z.undefined().optional(), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }).strict() as z.ZodType; /** @@ -200,6 +202,7 @@ export class CliCallTemplateSerializer extends Serializer { env_vars: obj.env_vars, working_dir: obj.working_dir, auth: obj.auth, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 62f9e21..d2ee9fc 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,5 +13,8 @@ export function register(override: boolean = false): void { CommunicationProtocol.communicationProtocols['cli'] = new CliCommunicationProtocol(); } +// Automatically register CLI plugins on import +register(); + export * from './cli_call_template'; export * from './cli_communication_protocol'; \ No newline at end of file diff --git a/packages/code-mode/package.json b/packages/code-mode/package.json index c55b5dd..30c409d 100644 --- a/packages/code-mode/package.json +++ b/packages/code-mode/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/code-mode", - "version": "1.0.5", + "version": "1.1.0", "description": "Code execution mode for UTCP - enables executing TypeScript code chains with tool access", "main": "dist/index.cjs", "module": "dist/index.js", @@ -40,7 +40,7 @@ } }, "dependencies": { - "@utcp/sdk": "^1.0.17" + "@utcp/sdk": "^1.1.0" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index bcb413f..e904881 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/sdk", - "version": "1.0.17", + "version": "1.1.0", "description": "Universal Tool Calling Protocol SDK", "main": "dist/index.cjs", "module": "dist/index.js", diff --git a/packages/core/src/client/utcp_client.ts b/packages/core/src/client/utcp_client.ts index afaf7c0..1162003 100644 --- a/packages/core/src/client/utcp_client.ts +++ b/packages/core/src/client/utcp_client.ts @@ -138,11 +138,26 @@ export class UtcpClient implements IUtcpClient { const result = await protocol.registerManual(this, processedCallTemplate); if (result.success) { + // Determine allowed protocols: use explicit list if provided, otherwise default to manual's own protocol + const allowedProtocols = (processedCallTemplate.allowed_communication_protocols?.length) + ? processedCallTemplate.allowed_communication_protocols + : [processedCallTemplate.call_template_type]; + + // Filter tools based on allowed protocols and prefix names + const filteredTools = []; for (const tool of result.manual.tools) { + const toolProtocol = tool.tool_call_template.call_template_type; + if (!allowedProtocols.includes(toolProtocol)) { + console.warn(`Tool '${tool.name}' uses communication protocol '${toolProtocol}' which is not in allowed protocols [${allowedProtocols.map(p => `'${p}'`).join(', ')}] for manual '${manualCallTemplate.name}'. Tool will not be registered.`); + continue; + } if (!tool.name.startsWith(`${processedCallTemplate.name}.`)) { tool.name = `${processedCallTemplate.name}.${tool.name}`; } + filteredTools.push(tool); } + result.manual.tools = filteredTools; + await this.config.tool_repository.saveManual(processedCallTemplate, result.manual); console.log(`Successfully registered manual '${manualCallTemplate.name}' with ${result.manual.tools.length} tools.`); } else { @@ -224,6 +239,16 @@ export class UtcpClient implements IUtcpClient { throw new Error(`Could not find manual call template for manual '${manualName}'.`); } + // Validate protocol is allowed + const toolProtocol = tool.tool_call_template.call_template_type; + const allowedProtocols = (manualCallTemplate.allowed_communication_protocols?.length) + ? manualCallTemplate.allowed_communication_protocols + : [manualCallTemplate.call_template_type]; + + if (!allowedProtocols.includes(toolProtocol)) { + throw new Error(`Tool '${toolName}' uses communication protocol '${toolProtocol}' which is not allowed by manual '${manualName}'. Allowed protocols: [${allowedProtocols.map(p => `'${p}'`).join(', ')}]`); + } + const processedToolCallTemplate = await this.substituteCallTemplateVariables(tool.tool_call_template, manualName); const protocol = this._registeredCommProtocols.get(processedToolCallTemplate.call_template_type); @@ -263,6 +288,16 @@ export class UtcpClient implements IUtcpClient { throw new Error(`Could not find manual call template for manual '${manualName}'.`); } + // Validate protocol is allowed + const toolProtocol = tool.tool_call_template.call_template_type; + const allowedProtocols = (manualCallTemplate.allowed_communication_protocols?.length) + ? manualCallTemplate.allowed_communication_protocols + : [manualCallTemplate.call_template_type]; + + if (!allowedProtocols.includes(toolProtocol)) { + throw new Error(`Tool '${toolName}' uses communication protocol '${toolProtocol}' which is not allowed by manual '${manualName}'. Allowed protocols: [${allowedProtocols.map(p => `'${p}'`).join(', ')}]`); + } + const processedToolCallTemplate = await this.substituteCallTemplateVariables(tool.tool_call_template, manualName); const protocol = this._registeredCommProtocols.get(processedToolCallTemplate.call_template_type); diff --git a/packages/core/src/data/call_template.ts b/packages/core/src/data/call_template.ts index d0f3597..73541d6 100644 --- a/packages/core/src/data/call_template.ts +++ b/packages/core/src/data/call_template.ts @@ -23,6 +23,18 @@ export interface CallTemplate { */ auth?: Auth; + /** + * Optional list of allowed communication protocol types for tools within this manual. + * + * Behavior: + * - If undefined, null, or empty array → defaults to only allowing the manual's own call_template_type + * - If set to a non-empty array → only those protocol types are allowed + * + * This provides secure-by-default behavior where a manual can only register/call tools + * that use its own protocol unless explicitly configured otherwise. + */ + allowed_communication_protocols?: string[]; + [key: string]: any; } diff --git a/packages/direct-call/package.json b/packages/direct-call/package.json index 8d30603..3e609c0 100644 --- a/packages/direct-call/package.json +++ b/packages/direct-call/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/direct-call", - "version": "1.0.12", + "version": "1.1.0", "description": "Direct callable functions plugin for UTCP", "main": "dist/index.cjs", "module": "dist/index.js", @@ -44,7 +44,7 @@ } }, "dependencies": { - "@utcp/sdk": "^1.0.15" + "@utcp/sdk": "^1.1.0" }, "devDependencies": { "bun-types": "latest", diff --git a/packages/direct-call/src/direct_call_template.ts b/packages/direct-call/src/direct_call_template.ts index c31652d..9be5f1b 100644 --- a/packages/direct-call/src/direct_call_template.ts +++ b/packages/direct-call/src/direct_call_template.ts @@ -17,6 +17,7 @@ export interface DirectCallTemplate extends CallTemplate { call_template_type: 'direct-call'; callable_name: string; auth?: undefined; + allowed_communication_protocols?: string[]; } /** @@ -27,6 +28,7 @@ export const DirectCallTemplateSchema: z.ZodType = z.object( call_template_type: z.literal('direct-call'), callable_name: z.string().describe('The name of the callable function to invoke.'), auth: z.undefined().optional(), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }).strict() as z.ZodType; /** @@ -42,6 +44,7 @@ export class DirectCallTemplateSerializer extends Serializer call_template_type: obj.call_template_type, callable_name: obj.callable_name, auth: obj.auth, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/packages/dotenv-loader/package.json b/packages/dotenv-loader/package.json index d3b636d..576b96e 100644 --- a/packages/dotenv-loader/package.json +++ b/packages/dotenv-loader/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/dotenv-loader", - "version": "1.0.1", + "version": "1.1.0", "description": "DotEnv Variable Loader plugin for UTCP", "main": "dist/index.cjs", "module": "dist/index.js", @@ -44,7 +44,7 @@ "zod": "^3.23.8" }, "peerDependencies": { - "@utcp/sdk": "^1.0.15" + "@utcp/sdk": "^1.1.0" }, "devDependencies": { "bun-types": "latest", diff --git a/packages/file/package.json b/packages/file/package.json index 53f9b6f..30174fc 100644 --- a/packages/file/package.json +++ b/packages/file/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/file", - "version": "1.0.1", + "version": "1.1.0", "description": "File system protocol for UTCP - reads UTCP manuals from local files (Node.js only)", "main": "dist/index.cjs", "module": "dist/index.js", @@ -43,8 +43,8 @@ } }, "dependencies": { - "@utcp/sdk": "^1.0.15", - "@utcp/http": "^1.0.13", + "@utcp/sdk": "^1.1.0", + "@utcp/http": "^1.1.0", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/packages/file/src/file_call_template.ts b/packages/file/src/file_call_template.ts index ed8e0dc..61b3b0c 100644 --- a/packages/file/src/file_call_template.ts +++ b/packages/file/src/file_call_template.ts @@ -23,6 +23,7 @@ export interface FileCallTemplate extends CallTemplate { file_path: string; auth?: undefined; auth_tools?: Auth | null; + allowed_communication_protocols?: string[]; } /** @@ -40,6 +41,7 @@ export const FileCallTemplateSchema: z.ZodType = z.object({ } return val as Auth; }).describe('Authentication to apply to generated tools from OpenAPI specs.'), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }).strict() as z.ZodType; /** @@ -58,6 +60,7 @@ export class FileCallTemplateSerializer extends Serializer { file_path: obj.file_path, auth: obj.auth, auth_tools: obj.auth_tools ? new AuthSerializer().toDict(obj.auth_tools) : null, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/packages/http/package.json b/packages/http/package.json index 80b6d6f..cff28f0 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/http", - "version": "1.0.13", + "version": "1.1.0", "description": "HTTP utilities for UTCP", "main": "dist/index.cjs", "module": "dist/index.js", @@ -43,7 +43,7 @@ } }, "dependencies": { - "@utcp/sdk": "^1.0.15", + "@utcp/sdk": "^1.1.0", "axios": "^1.11.0", "js-yaml": "^4.1.0" }, diff --git a/packages/http/src/http_call_template.ts b/packages/http/src/http_call_template.ts index e344dd9..93dbcba 100644 --- a/packages/http/src/http_call_template.ts +++ b/packages/http/src/http_call_template.ts @@ -22,6 +22,7 @@ export interface HttpCallTemplate extends CallTemplate { body_field?: string; header_fields?: string[]; auth_tools?: Auth | null; + allowed_communication_protocols?: string[]; } /** @@ -45,6 +46,7 @@ export const HttpCallTemplateSchema: z.ZodType = z.object({ } return val as Auth; }).describe('Authentication configuration for generated tools'), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }) as z.ZodType; /** @@ -64,6 +66,7 @@ export class HttpCallTemplateSerializer extends Serializer { headers: obj.headers, body_field: obj.body_field, header_fields: obj.header_fields, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/packages/http/src/sse_call_template.ts b/packages/http/src/sse_call_template.ts index 881d8d0..e1ce2bb 100644 --- a/packages/http/src/sse_call_template.ts +++ b/packages/http/src/sse_call_template.ts @@ -33,6 +33,7 @@ export interface SseCallTemplate extends CallTemplate { headers?: Record; body_field?: string | null; header_fields?: string[] | null; + allowed_communication_protocols?: string[]; } /** @@ -49,6 +50,7 @@ export const SseCallTemplateSchema: z.ZodType = z.object({ headers: z.record(z.string(), z.string()).optional().describe('Optional static headers for the initial connection.'), body_field: z.string().nullable().optional().describe('The name of the single input field to be sent as the request body.'), header_fields: z.array(z.string()).nullable().optional().describe('List of input fields to be sent as request headers for the initial connection.'), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }) as z.ZodType; /** @@ -72,6 +74,7 @@ export class SseCallTemplateSerializer extends Serializer { headers: obj.headers, body_field: obj.body_field, header_fields: obj.header_fields, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/packages/http/src/streamable_http_call_template.ts b/packages/http/src/streamable_http_call_template.ts index 0da3ddb..c78e6ff 100644 --- a/packages/http/src/streamable_http_call_template.ts +++ b/packages/http/src/streamable_http_call_template.ts @@ -35,6 +35,7 @@ export interface StreamableHttpCallTemplate extends CallTemplate { headers?: Record; body_field?: string | null; header_fields?: string[] | null; + allowed_communication_protocols?: string[]; } /** @@ -52,6 +53,7 @@ export const StreamableHttpCallTemplateSchema: z.ZodType; /** @@ -76,6 +78,7 @@ export class StreamableHttpCallTemplateSerializer extends Serializer = z.object({ config: McpConfigSchema.describe('Configuration object containing MCP server definitions. Follows the same format as the official MCP server configuration.'), auth: AuthSchema.nullable().optional().describe('Optional OAuth2 authentication for HTTP-based MCP servers.'), register_resources_as_tools: z.boolean().default(false).describe('Whether to register MCP resources as callable tools. When True, server resources are exposed as tools that can be called.'), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }) as z.ZodType; /** @@ -153,6 +155,7 @@ export class McpCallTemplateSerializer extends Serializer { config: obj.config, auth: obj.auth, register_resources_as_tools: obj.register_resources_as_tools, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/packages/text/package.json b/packages/text/package.json index d882c02..5d7d3bc 100644 --- a/packages/text/package.json +++ b/packages/text/package.json @@ -1,6 +1,6 @@ { "name": "@utcp/text", - "version": "1.0.12", + "version": "1.1.0", "description": "Text content protocol for UTCP - handles direct text content (browser-compatible)", "main": "dist/index.cjs", "module": "dist/index.js", @@ -38,8 +38,8 @@ } }, "dependencies": { - "@utcp/http": "^1.0.13", - "@utcp/sdk": "^1.0.15", + "@utcp/http": "^1.1.0", + "@utcp/sdk": "^1.1.0", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/packages/text/src/text_call_template.ts b/packages/text/src/text_call_template.ts index 8a47a5e..930c863 100644 --- a/packages/text/src/text_call_template.ts +++ b/packages/text/src/text_call_template.ts @@ -22,6 +22,7 @@ export interface TextCallTemplate extends CallTemplate { base_url?: string; auth?: undefined; auth_tools?: Auth | null; + allowed_communication_protocols?: string[]; } /** @@ -40,6 +41,7 @@ export const TextCallTemplateSchema: z.ZodType = z.object({ } return val as Auth; }).describe('Optional authentication to apply to generated tools from OpenAPI specs'), + allowed_communication_protocols: z.array(z.string()).optional().describe('Optional list of allowed communication protocol types for tools within this manual.'), }).strict() as z.ZodType; /** @@ -54,6 +56,7 @@ export class TextCallTemplateSerializer extends Serializer { base_url: obj.base_url, auth: obj.auth, auth_tools: obj.auth_tools ? new AuthSerializer().toDict(obj.auth_tools) : null, + allowed_communication_protocols: obj.allowed_communication_protocols, }; } diff --git a/scripts/update-versions.js b/scripts/update-versions.js index 8e1fffe..817c609 100644 --- a/scripts/update-versions.js +++ b/scripts/update-versions.js @@ -22,7 +22,7 @@ const PACKAGES_DIR = path.resolve(__dirname, '../packages'); const ROOT_DIR = path.resolve(__dirname, '..'); // Package directories -const PACKAGES = ['core', 'http', 'mcp', 'text', 'file', 'cli', 'direct-call', 'dotenv-loader']; +const PACKAGES = ['core', 'http', 'mcp', 'text', 'file', 'cli', 'direct-call', 'dotenv-loader', 'code-mode']; /** * Parse semantic version string diff --git a/tests/utcp_client.test.ts b/tests/utcp_client.test.ts index 8415098..4d88ec1 100644 --- a/tests/utcp_client.test.ts +++ b/tests/utcp_client.test.ts @@ -1,19 +1,21 @@ // packages/core/tests/utcp_client.test.ts -import { test, expect, afterAll, beforeAll, describe } from "bun:test"; +import { test, expect, afterAll, beforeAll, afterEach, describe } from "bun:test"; import { Subprocess } from "bun"; import path from "path"; import { writeFile, unlink } from "fs/promises"; -import { UtcpClient } from "@utcp/sdk"; +import { UtcpClient, CommunicationProtocol, RegisterManualResult, CallTemplate, UtcpManual, Tool, IUtcpClient } from "@utcp/sdk"; // Import protocol packages to register their serializers and communication protocols import "@utcp/http"; import "@utcp/file"; import "@utcp/mcp"; +import "@utcp/cli"; import "@utcp/dotenv-loader"; // Import types after registering the packages import type { McpCallTemplate } from "@utcp/mcp"; import type { HttpCallTemplate } from "@utcp/http"; import type { FileCallTemplate } from "@utcp/file"; +import type { CliCallTemplate } from "@utcp/cli"; let httpManualServerProcess: Subprocess | null = null; let mcpStdioServerProcess: Subprocess | null = null; @@ -357,6 +359,313 @@ describe("UtcpClient End-to-End Tests", () => { expect(searchResults.length).toBeGreaterThan(0); expect(searchResults[0]?.name).toBe("mcp_search_manual.mock_stdio_server.echo"); + await client.close(); + }); +}); + +// --- allowed_communication_protocols Tests --- +// These tests use mock communication protocols to verify protocol filtering behavior + +/** + * Mock communication protocol for testing allowed_communication_protocols. + * Returns a predefined manual on registration and a predefined result on tool calls. + */ +class MockCommunicationProtocol extends CommunicationProtocol { + private manual: UtcpManual; + private callResult: any; + + constructor(manual?: UtcpManual, callResult: any = "mock_result") { + super(); + this.manual = manual || { utcp_version: "1.0", manual_version: "1.0", tools: [] }; + this.callResult = callResult; + } + + async registerManual(caller: IUtcpClient, manualCallTemplate: CallTemplate): Promise { + return { + manualCallTemplate, + manual: this.manual, + success: true, + errors: [], + }; + } + + async deregisterManual(caller: IUtcpClient, manualCallTemplate: CallTemplate): Promise {} + + async callTool(caller: IUtcpClient, toolName: string, toolArgs: Record, toolCallTemplate: CallTemplate): Promise { + return this.callResult; + } + + async *callToolStreaming(caller: IUtcpClient, toolName: string, toolArgs: Record, toolCallTemplate: CallTemplate): AsyncGenerator { + yield this.callResult; + } +} + +describe("allowed_communication_protocols Tests", () => { + // Store original protocols to restore after tests + let originalProtocols: { [type: string]: CommunicationProtocol }; + + beforeAll(() => { + // Save original protocols + originalProtocols = { ...CommunicationProtocol.communicationProtocols }; + }); + + afterEach(() => { + // Restore original protocols after each test + CommunicationProtocol.communicationProtocols = { ...originalProtocols }; + }); + + test("should call tool when its protocol is in the allowed list", async () => { + console.log("\nRunning test: call_tool_allowed_protocol..."); + + // Create HTTP tool + const httpTool: Tool = { + name: "http_tool", + description: "HTTP test tool", + inputs: { type: "object", properties: { param1: { type: "string" } } }, + outputs: { type: "object", properties: {} }, + tags: ["http"], + tool_call_template: { + name: "http_provider", + call_template_type: "http", + url: "https://api.example.com/call", + http_method: "GET", + content_type: "application/json", + } as HttpCallTemplate, + }; + + const manual: UtcpManual = { utcp_version: "1.0", manual_version: "1.0", tools: [httpTool] }; + const mockProtocol = new MockCommunicationProtocol(manual, "test_result"); + CommunicationProtocol.communicationProtocols["http"] = mockProtocol; + + const client = await UtcpClient.create(process.cwd(), {}); + + await client.registerManual({ + name: "test_manual", + call_template_type: "http", + url: "https://api.example.com/tool", + http_method: "POST", + content_type: "application/json", + allowed_communication_protocols: ["http", "cli"], // Allow both HTTP and CLI + } as HttpCallTemplate); + + // Call should succeed since "http" is in allowed_communication_protocols + const result = await client.callTool("test_manual.http_tool", { param1: "value1" }); + expect(result).toBe("test_result"); + + await client.close(); + }); + + test("should filter out tools with disallowed protocols during registration", async () => { + console.log("\nRunning test: register_filters_disallowed_protocol_tools..."); + + // Create a CLI tool (which will not be allowed) + const cliTool: Tool = { + name: "cli_tool", + description: "CLI test tool", + inputs: { type: "object", properties: { command: { type: "string", description: "Command to execute" } }, required: ["command"] }, + outputs: { type: "object", properties: { output: { type: "string", description: "Command output" } } }, + tags: ["cli", "test"], + tool_call_template: { + name: "cli_provider", + call_template_type: "cli", + commands: [{ command: "echo UTCP_ARG_command_UTCP_END" }], + } as CliCallTemplate, + }; + + const manual: UtcpManual = { utcp_version: "1.0", manual_version: "1.0", tools: [cliTool] }; + const mockHttpProtocol = new MockCommunicationProtocol(manual); + const mockCliProtocol = new MockCommunicationProtocol(); + CommunicationProtocol.communicationProtocols["http"] = mockHttpProtocol; + CommunicationProtocol.communicationProtocols["cli"] = mockCliProtocol; + + const client = await UtcpClient.create(process.cwd(), {}); + + const result = await client.registerManual({ + name: "http_manual", + call_template_type: "http", + url: "https://api.example.com/tool", + http_method: "POST", + content_type: "application/json", + allowed_communication_protocols: ["http"], // Only allow HTTP + } as HttpCallTemplate); + + // CLI tool should be filtered out during registration + expect(result.manual.tools.length).toBe(0); + + // Tool should not exist in repository + const tool = await client.getTool("http_manual.cli_tool"); + expect(tool).toBeUndefined(); + + await client.close(); + }); + + test("should only allow manual's own protocol when no allowed_communication_protocols is set", async () => { + console.log("\nRunning test: call_tool_default_protocol_restriction..."); + + // Create tools: one HTTP (should be registered), one CLI (should be filtered out) + const httpTool: Tool = { + name: "http_tool", + description: "HTTP test tool", + inputs: { type: "object", properties: {} }, + outputs: { type: "object", properties: {} }, + tags: [], + tool_call_template: { + name: "http_provider", + call_template_type: "http", + url: "https://api.example.com/call", + http_method: "GET", + content_type: "application/json", + } as HttpCallTemplate, + }; + + const cliTool: Tool = { + name: "cli_tool", + description: "CLI test tool", + inputs: { type: "object", properties: {} }, + outputs: { type: "object", properties: {} }, + tags: [], + tool_call_template: { + name: "cli_provider", + call_template_type: "cli", + commands: [{ command: "echo test" }], + } as CliCallTemplate, + }; + + const manual: UtcpManual = { utcp_version: "1.0", manual_version: "1.0", tools: [httpTool, cliTool] }; + const mockHttpProtocol = new MockCommunicationProtocol(manual, "http_result"); + const mockCliProtocol = new MockCommunicationProtocol(); + CommunicationProtocol.communicationProtocols["http"] = mockHttpProtocol; + CommunicationProtocol.communicationProtocols["cli"] = mockCliProtocol; + + const client = await UtcpClient.create(process.cwd(), {}); + + // Register HTTP manual without explicit protocol restrictions + // Default behavior: only HTTP tools should be allowed + const result = await client.registerManual({ + name: "http_manual", + call_template_type: "http", + url: "https://api.example.com/tool", + http_method: "POST", + content_type: "application/json", + // No allowed_communication_protocols set - defaults to ["http"] + } as HttpCallTemplate); + + // Only HTTP tool should be registered, CLI tool should be filtered out + expect(result.manual.tools.length).toBe(1); + expect(result.manual.tools[0].name).toBe("http_manual.http_tool"); + + // HTTP tool call should succeed + const callResult = await client.callTool("http_manual.http_tool", {}); + expect(callResult).toBe("http_result"); + + // CLI tool should not exist in repository + const cliToolInRepo = await client.getTool("http_manual.cli_tool"); + expect(cliToolInRepo).toBeUndefined(); + + await client.close(); + }); + + test("should register tools from multiple protocols when explicitly allowed", async () => { + console.log("\nRunning test: register_with_multiple_allowed_protocols..."); + + const httpTool: Tool = { + name: "http_tool", + description: "HTTP test tool", + inputs: { type: "object", properties: {} }, + outputs: { type: "object", properties: {} }, + tags: [], + tool_call_template: { + name: "http_provider", + call_template_type: "http", + url: "https://api.example.com/call", + http_method: "GET", + content_type: "application/json", + } as HttpCallTemplate, + }; + + const cliTool: Tool = { + name: "cli_tool", + description: "CLI test tool", + inputs: { type: "object", properties: {} }, + outputs: { type: "object", properties: {} }, + tags: [], + tool_call_template: { + name: "cli_provider", + call_template_type: "cli", + commands: [{ command: "echo test" }], + } as CliCallTemplate, + }; + + const manual: UtcpManual = { utcp_version: "1.0", manual_version: "1.0", tools: [httpTool, cliTool] }; + const mockHttpProtocol = new MockCommunicationProtocol(manual, "http_result"); + const mockCliProtocol = new MockCommunicationProtocol(undefined, "cli_result"); + CommunicationProtocol.communicationProtocols["http"] = mockHttpProtocol; + CommunicationProtocol.communicationProtocols["cli"] = mockCliProtocol; + + const client = await UtcpClient.create(process.cwd(), {}); + + const result = await client.registerManual({ + name: "multi_protocol_manual", + call_template_type: "http", + url: "https://api.example.com/tool", + http_method: "POST", + content_type: "application/json", + allowed_communication_protocols: ["http", "cli"], // Allow both + } as HttpCallTemplate); + + // Both tools should be registered + expect(result.manual.tools.length).toBe(2); + const toolNames = result.manual.tools.map(t => t.name); + expect(toolNames).toContain("multi_protocol_manual.http_tool"); + expect(toolNames).toContain("multi_protocol_manual.cli_tool"); + + // Both tools should be callable + const httpResult = await client.callTool("multi_protocol_manual.http_tool", {}); + expect(httpResult).toBe("http_result"); + + const cliResult = await client.callTool("multi_protocol_manual.cli_tool", {}); + expect(cliResult).toBe("cli_result"); + + await client.close(); + }); + + test("should treat empty allowed_communication_protocols array as default (manual's own protocol)", async () => { + console.log("\nRunning test: call_tool_empty_allowed_protocols_defaults_to_manual_type..."); + + // Create a CLI tool (which will be filtered since manual is HTTP) + const cliTool: Tool = { + name: "cli_tool", + description: "CLI test tool", + inputs: { type: "object", properties: {} }, + outputs: { type: "object", properties: {} }, + tags: [], + tool_call_template: { + name: "cli_provider", + call_template_type: "cli", + commands: [{ command: "echo test" }], + } as CliCallTemplate, + }; + + const manual: UtcpManual = { utcp_version: "1.0", manual_version: "1.0", tools: [cliTool] }; + const mockHttpProtocol = new MockCommunicationProtocol(manual); + const mockCliProtocol = new MockCommunicationProtocol(undefined, "cli_result"); + CommunicationProtocol.communicationProtocols["http"] = mockHttpProtocol; + CommunicationProtocol.communicationProtocols["cli"] = mockCliProtocol; + + const client = await UtcpClient.create(process.cwd(), {}); + + const result = await client.registerManual({ + name: "http_manual", + call_template_type: "http", + url: "https://api.example.com/tool", + http_method: "POST", + content_type: "application/json", + allowed_communication_protocols: [], // Empty list defaults to ["http"] + } as HttpCallTemplate); + + // CLI tool should be filtered out during registration + expect(result.manual.tools.length).toBe(0); + await client.close(); }); }); \ No newline at end of file