@toolbox-sdk/core
Version:
JavaScript Base SDK for interacting with the Toolbox service
221 lines • 11.4 kB
JavaScript
// 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 axios from 'axios';
import { ZodManifestSchema, createZodSchemaFromParams, } from './protocol.js';
import { logApiError } from './errorUtils.js';
import { ZodError } from 'zod';
import { identifyAuthRequirements, resolveValue } from './utils.js';
/**
* An asynchronous client for interacting with a Toolbox service.
*/
class ToolboxClient {
#baseUrl;
#session;
#clientHeaders;
/**
* 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.
*/
constructor(url, session, clientHeaders) {
this.#baseUrl = url;
this.#session = session || axios.create({ baseURL: this.#baseUrl });
this.#clientHeaders = clientHeaders || {};
}
/**
* 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);
}
/**
* Fetches and parses the manifest from a given API path.
* @param {string} apiPath - The API path to fetch the manifest from (e.g., "/api/tool/mytool").
* @returns {Promise<Manifest>} A promise that resolves to the parsed manifest.
* @throws {Error} If there's an error fetching data or if the manifest structure is invalid.
*/
async #fetchAndParseManifest(apiPath) {
const url = `${this.#baseUrl}${apiPath}`;
try {
const headers = await this.#resolveClientHeaders();
const config = { headers };
const response = await this.#session.get(url, config);
const responseData = response.data;
try {
const manifest = ZodManifestSchema.parse(responseData);
return manifest;
}
catch (validationError) {
let detailedMessage = `Invalid manifest structure received from ${url}: `;
if (validationError instanceof ZodError) {
const issueDetails = validationError.issues;
detailedMessage += JSON.stringify(issueDetails, null, 2);
}
else if (validationError instanceof Error) {
detailedMessage += validationError.message;
}
else {
detailedMessage += 'Unknown validation error.';
}
throw new Error(detailedMessage);
}
}
catch (error) {
if (error instanceof Error &&
error.message.startsWith('Invalid manifest structure received from')) {
throw error;
}
logApiError(`Error fetching data from ${url}:`, error);
throw error;
}
}
/**
* 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 {ReturnType<typeof 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.#session, this.#baseUrl, toolName, toolSchema.description, paramZodSchema, authTokenGetters, remainingAuthnParams, remainingAuthzTokens, currBoundParams, this.#clientHeaders);
const usedBoundKeys = new Set(Object.keys(currBoundParams));
return { tool, usedAuthKeys, usedBoundKeys };
}
/**
* 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<ReturnType<typeof 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 = {}) {
const apiPath = `/api/tool/${name}`;
const manifest = await this.#fetchAndParseManifest(apiPath);
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 ${apiPath}.`);
}
}
/**
* 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<Array<ReturnType<typeof 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) {
const toolsetName = name || '';
const apiPath = `/api/toolset/${toolsetName}`;
const manifest = await this.#fetchAndParseManifest(apiPath);
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