UNPKG

@toolbox-sdk/core

Version:
282 lines 14.8 kB
// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ToolboxTool } from './tool.js'; import { createZodSchemaFromParams, Protocol, getSupportedMcpVersions, } from './protocol.js'; import { McpHttpTransportV20241105 } from './mcp/v20241105/mcp.js'; import { McpHttpTransportV20250618 } from './mcp/v20250618/mcp.js'; import { McpHttpTransportV20250326 } from './mcp/v20250326/mcp.js'; import { McpHttpTransportV20251125 } from './mcp/v20251125/mcp.js'; import { McpHttpTransportV20260728 } from './mcp/v20260728/mcp.js'; import { ProtocolNegotiationError } from './errorUtils.js'; import { identifyAuthRequirements, resolveValue, warnIfHttpAndHeaders, } from './utils.js'; /** * An asynchronous client for interacting with a Toolbox service. */ class ToolboxClient { #transport; #clientHeaders; #session; #baseUrl; #supportedProtocols; /** * The negotiated protocol version currently in use. */ get protocolVersion() { return this.#transport.protocolVersion; } /** * Initializes the ToolboxClient. * @param {string} url - The base URL for the Toolbox service API (e.g., "http://localhost:5000"). * @param {AxiosInstance} [session] - Optional Axios instance for making HTTP * requests. If not provided, a new one will be created. * @param {ClientHeadersConfig} [clientHeaders] - Optional initial headers to * be included in each request. * @param {string} [clientName] - Optional name of the client package. * @param {string} [clientVersion] - Optional version of the client package. */ constructor(url, session, clientHeaders, protocol = Protocol.MCP, clientName, clientVersion) { this.#baseUrl = url; this.#clientHeaders = clientHeaders || {}; this.#session = session || undefined; warnIfHttpAndHeaders(url, this.#clientHeaders); let initialProtocol; if (Array.isArray(protocol)) { if (protocol.length === 0) { throw new Error('Protocol array cannot be empty'); } const globalSupported = getSupportedMcpVersions(); for (const p of protocol) { if (!globalSupported.includes(p)) { throw new Error(`Invalid protocol version '${p}'. Must be one of: ${globalSupported.join(', ')}`); } } const requestedSet = new Set(protocol); const sorted = globalSupported.filter(globalVer => requestedSet.has(globalVer)); if (sorted.length === 0) { throw new Error('None of the provided protocols are supported'); } this.#supportedProtocols = sorted; initialProtocol = sorted[0]; // Start with the highest requested version } else { initialProtocol = protocol; const globalSupported = getSupportedMcpVersions(); if (!globalSupported.includes(initialProtocol)) { throw new Error(`Invalid protocol version '${initialProtocol}'`); } this.#supportedProtocols = globalSupported; } this.#transport = this.#createTransportWithProtocols(url, session || undefined, initialProtocol, clientName, clientVersion); } #createTransport(url, session, protocol, clientName, clientVersion) { switch (protocol) { case Protocol.MCP_v20241105: return new McpHttpTransportV20241105(url, session, protocol, clientName, clientVersion); case Protocol.MCP_v20250326: return new McpHttpTransportV20250326(url, session, protocol, clientName, clientVersion); case Protocol.MCP_v20250618: return new McpHttpTransportV20250618(url, session, protocol, clientName, clientVersion); case Protocol.MCP_v20251125: return new McpHttpTransportV20251125(url, session, protocol, clientName, clientVersion); case Protocol.MCP_v20260728: return new McpHttpTransportV20260728(url, session, protocol, clientName, clientVersion); default: throw new Error(`Unsupported MCP protocol version: ${protocol}`); } } #createTransportWithProtocols(url, session, protocol, clientName, clientVersion) { const transport = this.#createTransport(url, session, protocol, clientName, clientVersion); if (this.#supportedProtocols) { transport.supportedProtocols = this.#supportedProtocols; } return transport; } /** * Resolves client headers from their provider functions. * @returns {Promise<Record<string, string>>} A promise that resolves to the resolved headers. */ async #resolveClientHeaders() { const resolvedEntries = await Promise.all(Object.entries(this.#clientHeaders).map(async ([key, value]) => { const resolved = await resolveValue(value); return [key, String(resolved)]; })); return Object.fromEntries(resolvedEntries); } /** * Creates a ToolboxTool instance from its schema. * @param {string} toolName - The name of the tool. * @param {ToolSchemaFromManifest} toolSchema - The schema definition of the tool from the manifest. * @param {BoundParams} [boundParams] - A map of all candidate parameters to bind. * @returns {ToolboxTool} A ToolboxTool function. */ #createToolInstance(toolName, toolSchema, authTokenGetters = {}, boundParams = {}) { const params = []; const authParams = {}; const currBoundParams = {}; for (const p of toolSchema.parameters) { if (p.authSources && p.authSources.length > 0) { authParams[p.name] = p.authSources; } else if (boundParams && p.name in boundParams) { currBoundParams[p.name] = boundParams[p.name]; } else { params.push(p); } } const [remainingAuthnParams, remainingAuthzTokens, usedAuthKeys] = identifyAuthRequirements(authParams, toolSchema.authRequired || [], authTokenGetters ? Object.keys(authTokenGetters) : []); const paramZodSchema = createZodSchemaFromParams(params); const tool = ToolboxTool(this.#transport, toolName, toolSchema.description, paramZodSchema, authTokenGetters, remainingAuthnParams, remainingAuthzTokens, currBoundParams, this.#clientHeaders); const usedBoundKeys = new Set(Object.keys(currBoundParams)); return { tool, usedAuthKeys, usedBoundKeys }; } async #executeWithFallback(action) { while (true) { try { return await action(); } catch (e) { if (e instanceof ProtocolNegotiationError) { const serverVersion = e.fallbackVersion; let mutuallySupported = null; if (this.#supportedProtocols.includes(serverVersion)) { mutuallySupported = [serverVersion]; } else { const allVersions = getSupportedMcpVersions(); if (allVersions.includes(serverVersion)) { const idx = allVersions.indexOf(serverVersion); const serverSupported = allVersions.slice(idx); mutuallySupported = this.#supportedProtocols.filter(v => serverSupported.includes(v)); } } if (!mutuallySupported || mutuallySupported.length === 0) { throw new Error('No mutually supported protocol version'); } const fallbackProtocol = mutuallySupported[0]; if (fallbackProtocol === this.#transport.protocolVersion) { throw e; } this.#transport = this.#createTransportWithProtocols(this.#baseUrl, this.#session, fallbackProtocol); } else { throw e; } } } } /** * Asynchronously loads a tool from the server. * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the * tool remotely. * * @param {string} name - The unique name or identifier of the tool to load. * @param {AuthTokenGetters | null} [authTokenGetters] - Optional map of auth service names to token getters. * @param {BoundParams | null} [boundParams] - Optional parameters to pre-bind to the tool. * @returns {Promise<ToolboxTool>} A promise that resolves * to a ToolboxTool function, ready for execution. * @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid, * or if there's an error fetching data from the API. */ async loadTool(name, authTokenGetters = {}, boundParams = {}) { warnIfHttpAndHeaders(this.#transport.baseUrl, authTokenGetters); const headers = await this.#resolveClientHeaders(); const manifest = await this.#executeWithFallback(() => this.#transport.toolGet(name, headers)); if (manifest.tools && Object.prototype.hasOwnProperty.call(manifest.tools, name)) { const specificToolSchema = manifest.tools[name]; const { tool, usedAuthKeys, usedBoundKeys } = this.#createToolInstance(name, specificToolSchema, authTokenGetters || undefined, boundParams || {}); const providedAuthKeys = new Set(authTokenGetters ? Object.keys(authTokenGetters) : []); const providedBoundKeys = new Set(boundParams ? Object.keys(boundParams) : []); const unusedAuth = [...providedAuthKeys].filter(key => !usedAuthKeys.has(key)); const unusedBound = [...providedBoundKeys].filter(key => !usedBoundKeys.has(key)); const errorMessages = []; if (unusedAuth.length > 0) { errorMessages.push(`unused auth tokens: ${unusedAuth.join(', ')}`); } if (unusedBound.length > 0) { errorMessages.push(`unused bound parameters: ${unusedBound.join(', ')}`); } if (errorMessages.length > 0) { throw new Error(`Validation failed for tool '${name}': ${errorMessages.join('; ')}.`); } return tool; } else { throw new Error(`Tool "${name}" not found in manifest from ${this.#transport.baseUrl}/api/tool/${name}.`); } } /** * Asynchronously fetches a toolset and loads all tools defined within it. * * @param {string | null} [name] - Name of the toolset to load. If null or undefined, loads the default toolset. * @param {AuthTokenGetters | null} [authTokenGetters] - Optional map of auth service names to token getters. * @param {BoundParams | null} [boundParams] - Optional parameters to pre-bind to the tools in the toolset. * @param {boolean} [strict=false] - If true, throws an error if any provided auth token or bound param is not used by at least one tool. * @returns {Promise<ToolboxTool[]>} A promise that resolves * to a list of ToolboxTool functions, ready for execution. * @throws {Error} If the manifest structure is invalid or if there's an error fetching data from the API. */ async loadToolset(name, authTokenGetters = {}, boundParams = {}, strict = false) { warnIfHttpAndHeaders(this.#transport.baseUrl, authTokenGetters); const toolsetName = name || ''; const headers = await this.#resolveClientHeaders(); const manifest = await this.#executeWithFallback(() => this.#transport.toolsList(toolsetName, headers)); const tools = []; const overallUsedAuthKeys = new Set(); const overallUsedBoundParams = new Set(); const providedAuthKeys = new Set(authTokenGetters ? Object.keys(authTokenGetters) : []); const providedBoundKeys = new Set(boundParams ? Object.keys(boundParams) : []); for (const [toolName, toolSchema] of Object.entries(manifest.tools)) { const { tool, usedAuthKeys, usedBoundKeys } = this.#createToolInstance(toolName, toolSchema, authTokenGetters || {}, boundParams || {}); tools.push(tool); if (strict) { const unusedAuth = [...providedAuthKeys].filter(key => !usedAuthKeys.has(key)); const unusedBound = [...providedBoundKeys].filter(key => !usedBoundKeys.has(key)); const errorMessages = []; if (unusedAuth.length > 0) { errorMessages.push(`unused auth tokens: ${unusedAuth.join(', ')}`); } if (unusedBound.length > 0) { errorMessages.push(`unused bound parameters: ${unusedBound.join(', ')}`); } if (errorMessages.length > 0) { throw new Error(`Validation failed for tool '${toolName}': ${errorMessages.join('; ')}.`); } } else { usedAuthKeys.forEach(key => overallUsedAuthKeys.add(key)); usedBoundKeys.forEach(key => overallUsedBoundParams.add(key)); } } if (!strict) { const unusedAuth = [...providedAuthKeys].filter(key => !overallUsedAuthKeys.has(key)); const unusedBound = [...providedBoundKeys].filter(key => !overallUsedBoundParams.has(key)); const errorMessages = []; if (unusedAuth.length > 0) { errorMessages.push(`unused auth tokens could not be applied to any tool: ${unusedAuth.join(', ')}`); } if (unusedBound.length > 0) { errorMessages.push(`unused bound parameters could not be applied to any tool: ${unusedBound.join(', ')}`); } if (errorMessages.length > 0) { throw new Error(`Validation failed for toolset '${name || 'default'}': ${errorMessages.join('; ')}.`); } } return tools; } } export { ToolboxClient }; //# sourceMappingURL=client.js.map