Skip to content

Commit edb347c

Browse files
authored
feat(toolbox-core): add client headers (#23)
* feat: add client headers * lint * lint * resolve comments * disable any lint warning on a line * Only intercept request if it is sent to the toolbox server * feat(toolbox-core): Add helper methods for retrieving Google ID Tokens (#32) * working code * fix tests * lint * Revert "feat(toolbox-core): Add helper methods for retrieving Google ID Token…" (#39) This reverts commit f747e60. * remove addHeaders method * fix tests. Still need to update to use # for private vars * fix tests * minor cleanup * lint * do not use interceptors * Update client.ts * lint
1 parent 68105c1 commit edb347c

File tree

4 files changed

+244
-283
lines changed

4 files changed

+244
-283
lines changed

packages/toolbox-core/src/toolbox_core/client.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,62 @@
1414

1515
import {ToolboxTool} from './tool.js';
1616
import axios from 'axios';
17-
import {type AxiosInstance, type AxiosResponse} from 'axios';
17+
import {
18+
type AxiosInstance,
19+
type AxiosRequestConfig,
20+
type AxiosResponse,
21+
} from 'axios';
1822
import {ZodManifestSchema, createZodSchemaFromParams} from './protocol.js';
1923
import {logApiError} from './errorUtils.js';
2024
import {ZodError} from 'zod';
21-
import {BoundParams, BoundValue} from './utils.js';
25+
import {BoundParams, BoundValue, resolveValue} from './utils.js';
2226

2327
type Manifest = import('zod').infer<typeof ZodManifestSchema>;
2428
type ToolSchemaFromManifest = Manifest['tools'][string];
2529

30+
// Types for dynamic headers
31+
export type HeaderFunction = () => string | Promise<string>;
32+
export type ClientHeaderProvider = string | HeaderFunction;
33+
export type ClientHeadersConfig = Record<string, ClientHeaderProvider>;
34+
2635
/**
2736
* An asynchronous client for interacting with a Toolbox service.
28-
* Manages an Axios Client Session, if not provided.
2937
*/
3038
class ToolboxClient {
3139
#baseUrl: string;
3240
#session: AxiosInstance;
41+
#clientHeaders: ClientHeadersConfig;
3342

3443
/**
3544
* Initializes the ToolboxClient.
3645
* @param {string} url - The base URL for the Toolbox service API (e.g., "http://localhost:5000").
3746
* @param {AxiosInstance} [session] - Optional Axios instance for making HTTP
3847
* requests. If not provided, a new one will be created.
48+
* @param {ClientHeadersConfig} [clientHeaders] - Optional initial headers to
49+
* be included in each request.
3950
*/
40-
constructor(url: string, session?: AxiosInstance) {
51+
constructor(
52+
url: string,
53+
session?: AxiosInstance | null,
54+
clientHeaders?: ClientHeadersConfig | null
55+
) {
4156
this.#baseUrl = url;
4257
this.#session = session || axios.create({baseURL: this.#baseUrl});
58+
this.#clientHeaders = clientHeaders || {};
59+
}
60+
61+
/**
62+
* Resolves client headers from their provider functions.
63+
* @returns {Promise<Record<string, string>>} A promise that resolves to the resolved headers.
64+
*/
65+
async #resolveClientHeaders(): Promise<Record<string, string>> {
66+
const resolvedEntries = await Promise.all(
67+
Object.entries(this.#clientHeaders).map(async ([key, value]) => {
68+
const resolved = await resolveValue(value);
69+
return [key, String(resolved)];
70+
})
71+
);
72+
return Object.fromEntries(resolvedEntries);
4373
}
4474

4575
/**
@@ -51,7 +81,9 @@ class ToolboxClient {
5181
async #fetchAndParseManifest(apiPath: string): Promise<Manifest> {
5282
const url = `${this.#baseUrl}${apiPath}`;
5383
try {
54-
const response: AxiosResponse = await this.#session.get(url);
84+
const headers = await this.#resolveClientHeaders();
85+
const config: AxiosRequestConfig = {headers};
86+
const response: AxiosResponse = await this.#session.get(url, config);
5587
const responseData = response.data;
5688

5789
try {
@@ -114,7 +146,8 @@ class ToolboxClient {
114146
toolName,
115147
toolSchema.description,
116148
paramZodSchema,
117-
boundParams
149+
applicableBoundParams,
150+
this.#clientHeaders
118151
);
119152
return {tool, usedBoundKeys};
120153
}
@@ -125,8 +158,8 @@ class ToolboxClient {
125158
* returns a callable (`ToolboxTool`) that can be used to invoke the
126159
* tool remotely.
127160
*
128-
* @param {BoundParams} [boundParams] - Optional parameters to pre-bind to the tool.
129161
* @param {string} name - The unique name or identifier of the tool to load.
162+
* @param {BoundParams} [boundParams] - Optional parameters to pre-bind to the tool.
130163
* @returns {Promise<ReturnType<typeof ToolboxTool>>} A promise that resolves
131164
* to a ToolboxTool function, ready for execution.
132165
* @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid,
@@ -157,7 +190,9 @@ class ToolboxClient {
157190

158191
if (unusedBound.length > 0) {
159192
throw new Error(
160-
`Validation failed for tool '${name}': unused bound parameters: ${unusedBound.join(', ')}.`
193+
`Validation failed for tool '${name}': unused bound parameters: ${unusedBound.join(
194+
', '
195+
)}.`
161196
);
162197
}
163198
return tool;
@@ -205,7 +240,9 @@ class ToolboxClient {
205240
throw new Error(
206241
`Validation failed for toolset '${
207242
name || 'default'
208-
}': unused bound parameters could not be applied to any tool: ${unusedBound.join(', ')}.`
243+
}': unused bound parameters could not be applied to any tool: ${unusedBound.join(
244+
', '
245+
)}.`
209246
);
210247
}
211248
return tools;

packages/toolbox-core/src/toolbox_core/tool.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
// limitations under the License.
1414

1515
import {ZodObject, ZodError, ZodRawShape} from 'zod';
16-
import {AxiosInstance, AxiosResponse} from 'axios';
16+
import {AxiosInstance, AxiosRequestConfig, AxiosResponse} from 'axios';
1717
import {logApiError} from './errorUtils.js';
1818
import {BoundParams, BoundValue, resolveValue} from './utils.js';
19+
import {ClientHeadersConfig} from './client.js';
1920

2021
/**
2122
* Creates a callable tool function representing a specific tool on a remote
@@ -27,6 +28,7 @@ import {BoundParams, BoundValue, resolveValue} from './utils.js';
2728
* @param {string} description - A description of the remote tool.
2829
* @param {ZodObject<any>} paramSchema - The Zod schema for validating the tool's parameters.
2930
* @param {BoundParams} [boundParams] - Optional parameters to pre-bind to the tool.
31+
* @param {ClientHeadersConfig} [clientHeaders] - Optional client-specific headers.
3032
* @returns {CallableTool & CallableToolProperties} An async function that, when
3133
* called, invokes the tool with the provided arguments. Validates arguments
3234
* against the tool's signature, then sends them
@@ -39,7 +41,8 @@ function ToolboxTool(
3941
name: string,
4042
description: string,
4143
paramSchema: ZodObject<ZodRawShape>,
42-
boundParams: BoundParams = {}
44+
boundParams: BoundParams = {},
45+
clientHeaders: ClientHeadersConfig = {}
4346
) {
4447
const toolUrl = `${baseUrl}/api/tool/${name}/invoke`;
4548
const boundKeys = Object.keys(boundParams);
@@ -59,23 +62,42 @@ function ToolboxTool(
5962
e => `${e.path.join('.') || 'payload'}: ${e.message}`
6063
);
6164
throw new Error(
62-
`Argument validation failed for tool "${name}":\n - ${errorMessages.join('\n - ')}`
65+
`Argument validation failed for tool "${name}":\n - ${errorMessages.join(
66+
'\n - '
67+
)}`
6368
);
6469
}
6570
throw new Error(`Argument validation failed: ${String(error)}`);
6671
}
6772

6873
// Resolve any bound parameters that are functions.
69-
const resolvedEntries = await Promise.all(
74+
const resolvedBoundEntries = await Promise.all(
7075
Object.entries(boundParams).map(async ([key, value]) => {
7176
const resolved = await resolveValue(value);
7277
return [key, resolved];
7378
})
7479
);
75-
const resolvedBoundParams = Object.fromEntries(resolvedEntries);
80+
const resolvedBoundParams = Object.fromEntries(resolvedBoundEntries);
81+
82+
// Resolve client headers
83+
const resolvedHeaderEntries = await Promise.all(
84+
Object.entries(clientHeaders).map(async ([key, value]) => {
85+
const resolved = await resolveValue(value);
86+
return [key, resolved];
87+
})
88+
);
89+
const resolvedHeaders = Object.fromEntries(resolvedHeaderEntries);
90+
7691
const payload = {...validatedUserArgs, ...resolvedBoundParams};
7792
try {
78-
const response: AxiosResponse = await session.post(toolUrl, payload);
93+
const config: AxiosRequestConfig = {
94+
headers: resolvedHeaders,
95+
};
96+
const response: AxiosResponse = await session.post(
97+
toolUrl,
98+
payload,
99+
config
100+
);
79101
return response.data;
80102
} catch (error) {
81103
logApiError(`Error posting data to ${toolUrl}:`, error);
@@ -119,7 +141,8 @@ function ToolboxTool(
119141
this.toolName,
120142
this.description,
121143
this.params,
122-
newBoundParams
144+
newBoundParams,
145+
clientHeaders
123146
);
124147
};
125148

0 commit comments

Comments
 (0)