@modelprofile.com/flexharness 10.20.0 → 10.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/interfaces.d.ts +21 -0
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/utils.json.js +19 -1
- package/dist_ts/utils.sessionmetrics.d.ts +5 -0
- package/dist_ts/utils.sessionmetrics.js +26 -1
- package/dist_ts_mcp/index.d.ts +53 -0
- package/dist_ts_mcp/index.js +140 -44
- package/dist_ts_mcp/plugins.d.ts +3 -0
- package/dist_ts_mcp/plugins.js +4 -1
- package/package.json +3 -3
- package/readme.md +9 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/interfaces.ts +21 -0
- package/ts/readme.md +9 -2
- package/ts/utils.json.ts +19 -0
- package/ts/utils.sessionmetrics.ts +30 -0
- package/ts_mcp/index.ts +222 -52
- package/ts_mcp/plugins.ts +6 -0
- package/ts_mcp/readme.md +8 -0
package/ts_mcp/index.ts
CHANGED
|
@@ -124,62 +124,30 @@ export const createMcpTools = async (
|
|
|
124
124
|
|
|
125
125
|
const exposedTools: plugins.ToolSet = {};
|
|
126
126
|
const toolNameMap: Record<string, IMcpToolNameMapping> = {};
|
|
127
|
-
const usedToolNames = new Set<string>(['invalid']);
|
|
128
|
-
const formatResult = options.formatResult ?? formatMcpToolResult;
|
|
129
127
|
const shouldPrefixToolNames = options.prefixToolNames ?? connectedServers.length > 1;
|
|
130
128
|
|
|
131
|
-
for (const server of connectedServers) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
for (const { server, tool: mcpTool, exposedToolName } of nameMcpTools(connectedServers, shouldPrefixToolNames)) {
|
|
130
|
+
toolNameMap[exposedToolName] = {
|
|
131
|
+
serverName: server.name,
|
|
132
|
+
toolName: mcpTool.name,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
exposedTools[exposedToolName] = plugins.tool({
|
|
136
|
+
description: mcpTool.description ?? `MCP tool ${mcpTool.name} from ${server.name}`,
|
|
137
|
+
inputSchema: plugins.jsonSchema(mcpTool.inputSchema as never),
|
|
138
|
+
execute: async (input: unknown, invocationOptions?: plugins.ToolExecutionOptions<unknown>) => callMcpTool({
|
|
137
139
|
serverName: server.name,
|
|
138
140
|
toolName: mcpTool.name,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
exposedToolName,
|
|
150
|
-
arguments: normalizedArguments,
|
|
151
|
-
tool: mcpTool,
|
|
152
|
-
toolCallId: invocationOptions?.toolCallId ?? '',
|
|
153
|
-
abortSignal: invocationOptions?.abortSignal,
|
|
154
|
-
});
|
|
155
|
-
const configuredCallToolOptions: plugins.RequestOptions = {
|
|
156
|
-
...options.requestOptions,
|
|
157
|
-
...options.callToolRequestOptions,
|
|
158
|
-
};
|
|
159
|
-
const result = await withMcpRequestOptions(
|
|
160
|
-
configuredCallToolOptions,
|
|
161
|
-
invocationOptions?.abortSignal,
|
|
162
|
-
(requestOptions) => server.client.callTool(
|
|
163
|
-
{
|
|
164
|
-
name: mcpTool.name,
|
|
165
|
-
arguments: normalizedArguments,
|
|
166
|
-
},
|
|
167
|
-
undefined,
|
|
168
|
-
requestOptions,
|
|
169
|
-
),
|
|
170
|
-
);
|
|
171
|
-
const formattedResult = formatResult(result, {
|
|
172
|
-
serverName: server.name,
|
|
173
|
-
toolName: mcpTool.name,
|
|
174
|
-
exposedToolName,
|
|
175
|
-
});
|
|
176
|
-
if ((result as Record<string, unknown>).isError === true) {
|
|
177
|
-
throw new Error(formatMcpError(formattedResult));
|
|
178
|
-
}
|
|
179
|
-
return formattedResult;
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
}
|
|
141
|
+
tool: mcpTool,
|
|
142
|
+
exposedToolName,
|
|
143
|
+
arguments: normalizeMcpArguments(input),
|
|
144
|
+
toolCallId: invocationOptions?.toolCallId ?? '',
|
|
145
|
+
abortSignal: invocationOptions?.abortSignal,
|
|
146
|
+
authorize: (context) => options.authorizeToolCall?.(context),
|
|
147
|
+
client: () => server.client,
|
|
148
|
+
options,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
183
151
|
}
|
|
184
152
|
|
|
185
153
|
return {
|
|
@@ -197,6 +165,114 @@ export const createMcpTools = async (
|
|
|
197
165
|
}
|
|
198
166
|
};
|
|
199
167
|
|
|
168
|
+
/** The tools one MCP server lists, as `listTools` answered them. `IMcpConnectedServer` is one. */
|
|
169
|
+
export interface IMcpToolCatalogServer {
|
|
170
|
+
name: string;
|
|
171
|
+
tools: readonly TMcpToolMetadata[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface IMcpToolCatalogToolContext extends IMcpToolFormatContext {
|
|
175
|
+
tool: TMcpToolMetadata;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A catalog call of an MCP tool: the run's `call` context names the scope, session, run and `requestPermission`. */
|
|
179
|
+
export interface IMcpToolCatalogCallContext<TScope = unknown> extends IMcpToolFormatContext {
|
|
180
|
+
call: plugins.IFlexCatalogRunContext<TScope>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface IMcpToolCatalogAuthorizeToolCallContext<TScope = unknown> extends IMcpAuthorizeToolCallContext {
|
|
184
|
+
abortSignal: AbortSignal;
|
|
185
|
+
call: plugins.IFlexCatalogRunContext<TScope>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ICreateMcpToolCatalogOptions<TScope = unknown> {
|
|
189
|
+
/** The servers and the tools they list, in the order `createMcpTools` was given them. */
|
|
190
|
+
servers: readonly IMcpToolCatalogServer[];
|
|
191
|
+
/**
|
|
192
|
+
* The client that runs a call: the one connected for the call's run, which `call.provider.runId`
|
|
193
|
+
* names. The catalog stays the same object across runs; only the clients change.
|
|
194
|
+
*/
|
|
195
|
+
resolveClient(context: IMcpToolCatalogCallContext<TScope>): IMcpClientLike | PromiseLike<IMcpClientLike>;
|
|
196
|
+
/** As for `createMcpTools`: `<server>__<tool>`, by default when there is more than one server. */
|
|
197
|
+
prefixToolNames?: boolean;
|
|
198
|
+
requestOptions?: plugins.RequestOptions;
|
|
199
|
+
callToolRequestOptions?: plugins.RequestOptions;
|
|
200
|
+
formatResult?: (result: TMcpCallToolResult, context: IMcpToolFormatContext) => unknown;
|
|
201
|
+
/**
|
|
202
|
+
* Resolve to allow the call, or throw to deny it, before the client is resolved. It sees `execute`
|
|
203
|
+
* and `run_code` calls as `createMcpTools`' hook sees tool calls. Omission allows all calls.
|
|
204
|
+
*/
|
|
205
|
+
authorizeToolCall?: (context: IMcpToolCatalogAuthorizeToolCallContext<TScope>) => PromiseLike<void> | void;
|
|
206
|
+
/**
|
|
207
|
+
* Declares a tool's method read-only: its calls run concurrently with other reads and a turn
|
|
208
|
+
* answers identical ones once. A tool whose calls ask for permission must not be. Omission
|
|
209
|
+
* declares none; a server's `readOnlyHint` annotation is not trusted for it.
|
|
210
|
+
*/
|
|
211
|
+
isReadOnly?: (context: IMcpToolCatalogToolContext) => boolean;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface ICreateMcpToolCatalogResult<TScope = unknown> {
|
|
215
|
+
catalog: plugins.IFlexToolCatalog<TScope>;
|
|
216
|
+
/** The server and tool of each method path. */
|
|
217
|
+
toolNameMap: Record<string, IMcpToolNameMapping>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The MCP tools of `servers` as a tool catalog, which the model searches, describes and calls through
|
|
222
|
+
* `execute` and `run_code`. A method's path is the name `createMcpTools` gives the tool; a name that
|
|
223
|
+
* is no identifier (with `-`, or a reserved word) is declared as `FlexToolCatalog.identifierOf()`
|
|
224
|
+
* makes it. Create one catalog per set of tool definitions and return the same object on every run.
|
|
225
|
+
*/
|
|
226
|
+
export const createMcpToolCatalog = <TScope = unknown>(
|
|
227
|
+
options: ICreateMcpToolCatalogOptions<TScope>,
|
|
228
|
+
): ICreateMcpToolCatalogResult<TScope> => {
|
|
229
|
+
if (typeof options?.resolveClient !== 'function') {
|
|
230
|
+
throw new Error('createMcpToolCatalog requires resolveClient().');
|
|
231
|
+
}
|
|
232
|
+
const shouldPrefixToolNames = options.prefixToolNames ?? options.servers.length > 1;
|
|
233
|
+
const named = nameMcpTools(options.servers, shouldPrefixToolNames);
|
|
234
|
+
// A name that is an identifier is its path; a mapped one gets a path no such name has.
|
|
235
|
+
const usedPaths = new Set(named.map(({ exposedToolName }) => exposedToolName)
|
|
236
|
+
.filter((exposedToolName) => catalogPathOf(exposedToolName) === exposedToolName));
|
|
237
|
+
const toolNameMap: Record<string, IMcpToolNameMapping> = {};
|
|
238
|
+
const methods: plugins.IFlexCatalogMethod<TScope, Record<string, unknown>>[] = [];
|
|
239
|
+
for (const { server, tool: mcpTool, exposedToolName } of named) {
|
|
240
|
+
const identifier = catalogPathOf(exposedToolName);
|
|
241
|
+
const path = identifier === exposedToolName ? identifier : createUniqueToolName(identifier, usedPaths);
|
|
242
|
+
usedPaths.add(path);
|
|
243
|
+
toolNameMap[path] = { serverName: server.name, toolName: mcpTool.name };
|
|
244
|
+
const toolContext: IMcpToolCatalogToolContext = {
|
|
245
|
+
serverName: server.name,
|
|
246
|
+
toolName: mcpTool.name,
|
|
247
|
+
exposedToolName: path,
|
|
248
|
+
tool: mcpTool,
|
|
249
|
+
};
|
|
250
|
+
const text = mcpTool.description?.trim() || `MCP tool ${mcpTool.name} from ${server.name}`;
|
|
251
|
+
const summary = summaryOf(text);
|
|
252
|
+
const description = text === summary ? undefined : truncateUtf8(text, plugins.FLEX_TOOL_CATALOG_LIMITS.maxDescriptionBytes);
|
|
253
|
+
methods.push({
|
|
254
|
+
method: path,
|
|
255
|
+
summary,
|
|
256
|
+
...(description ? { description } : {}),
|
|
257
|
+
inputSchema: mcpTool.inputSchema,
|
|
258
|
+
...(options.isReadOnly?.(toolContext) ? { readOnly: true } : {}),
|
|
259
|
+
run: (params, call) => callMcpTool({
|
|
260
|
+
serverName: server.name,
|
|
261
|
+
toolName: mcpTool.name,
|
|
262
|
+
tool: mcpTool,
|
|
263
|
+
exposedToolName: path,
|
|
264
|
+
arguments: normalizeMcpArguments(params),
|
|
265
|
+
toolCallId: call.toolCallId,
|
|
266
|
+
abortSignal: call.abortSignal,
|
|
267
|
+
authorize: (context) => options.authorizeToolCall?.({ ...context, abortSignal: call.abortSignal, call }),
|
|
268
|
+
client: () => options.resolveClient({ serverName: server.name, toolName: mcpTool.name, exposedToolName: path, call }),
|
|
269
|
+
options,
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return { catalog: { methods }, toolNameMap };
|
|
274
|
+
};
|
|
275
|
+
|
|
200
276
|
export const sanitizeMcpToolName = (name: string, fallback = 'mcp_tool'): string => {
|
|
201
277
|
const sanitized = name
|
|
202
278
|
.trim()
|
|
@@ -340,6 +416,100 @@ const normalizeMaxListToolsPages = (maxPages = DEFAULT_MAX_LIST_TOOLS_PAGES): nu
|
|
|
340
416
|
return maxPages;
|
|
341
417
|
};
|
|
342
418
|
|
|
419
|
+
/** Every tool of `servers` with the name it is exposed under, unique across them, in order. */
|
|
420
|
+
const nameMcpTools = <TServer extends IMcpToolCatalogServer>(
|
|
421
|
+
servers: readonly TServer[],
|
|
422
|
+
prefixToolNames: boolean,
|
|
423
|
+
): Array<{ server: TServer; tool: TMcpToolMetadata; exposedToolName: string }> => {
|
|
424
|
+
const usedToolNames = new Set<string>(['invalid']);
|
|
425
|
+
const named: Array<{ server: TServer; tool: TMcpToolMetadata; exposedToolName: string }> = [];
|
|
426
|
+
for (const server of servers) {
|
|
427
|
+
for (const mcpTool of server.tools) {
|
|
428
|
+
const exposedToolName = createUniqueToolName(
|
|
429
|
+
createExposedToolName(server.name, mcpTool.name, prefixToolNames),
|
|
430
|
+
usedToolNames,
|
|
431
|
+
);
|
|
432
|
+
usedToolNames.add(exposedToolName);
|
|
433
|
+
named.push({ server, tool: mcpTool, exposedToolName });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return named;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
interface IMcpToolCall extends IMcpToolFormatContext {
|
|
440
|
+
tool: TMcpToolMetadata;
|
|
441
|
+
arguments: Record<string, unknown>;
|
|
442
|
+
toolCallId: string;
|
|
443
|
+
abortSignal?: AbortSignal;
|
|
444
|
+
authorize: (context: IMcpAuthorizeToolCallContext) => PromiseLike<void> | void;
|
|
445
|
+
client: () => IMcpClientLike | PromiseLike<IMcpClientLike>;
|
|
446
|
+
options: Pick<ICreateMcpToolsOptions, 'requestOptions' | 'callToolRequestOptions' | 'formatResult'>;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Authorizes a call, runs it on the client and formats its result; an MCP error result throws. */
|
|
450
|
+
const callMcpTool = async (call: IMcpToolCall): Promise<unknown> => {
|
|
451
|
+
await call.authorize({
|
|
452
|
+
serverName: call.serverName,
|
|
453
|
+
toolName: call.toolName,
|
|
454
|
+
exposedToolName: call.exposedToolName,
|
|
455
|
+
arguments: call.arguments,
|
|
456
|
+
tool: call.tool,
|
|
457
|
+
toolCallId: call.toolCallId,
|
|
458
|
+
abortSignal: call.abortSignal,
|
|
459
|
+
});
|
|
460
|
+
const client = await call.client();
|
|
461
|
+
const configuredCallToolOptions: plugins.RequestOptions = {
|
|
462
|
+
...call.options.requestOptions,
|
|
463
|
+
...call.options.callToolRequestOptions,
|
|
464
|
+
};
|
|
465
|
+
const result = await withMcpRequestOptions(
|
|
466
|
+
configuredCallToolOptions,
|
|
467
|
+
call.abortSignal,
|
|
468
|
+
(requestOptions) => client.callTool(
|
|
469
|
+
{
|
|
470
|
+
name: call.toolName,
|
|
471
|
+
arguments: call.arguments,
|
|
472
|
+
},
|
|
473
|
+
undefined,
|
|
474
|
+
requestOptions,
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
const formattedResult = (call.options.formatResult ?? formatMcpToolResult)(result, {
|
|
478
|
+
serverName: call.serverName,
|
|
479
|
+
toolName: call.toolName,
|
|
480
|
+
exposedToolName: call.exposedToolName,
|
|
481
|
+
});
|
|
482
|
+
if ((result as Record<string, unknown>).isError === true) {
|
|
483
|
+
throw new Error(formatMcpError(formattedResult));
|
|
484
|
+
}
|
|
485
|
+
return formattedResult;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/** A catalog method path: the exposed name where it is an identifier, else the identifier made of it. */
|
|
489
|
+
const catalogPathOf = (exposedToolName: string): string => plugins.FlexToolCatalog.identifierOf(exposedToolName);
|
|
490
|
+
|
|
491
|
+
/** The first line of a description, as one line of at most the catalog's summary length. */
|
|
492
|
+
const summaryOf = (text: string): string => {
|
|
493
|
+
const line = text.split(/\r?\n/).find((candidate) => candidate.trim())!.replace(/[\s\u0000-\u001f\u007f]+/g, ' ').trim();
|
|
494
|
+
const characters = [...line];
|
|
495
|
+
const max = plugins.FLEX_TOOL_CATALOG_LIMITS.maxSummaryCharacters;
|
|
496
|
+
return characters.length <= max ? line : `${characters.slice(0, max - 1).join('').trimEnd()}…`;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/** The text cut to at most `maxBytes` UTF-8 bytes on a character boundary, marked with `…` when cut. */
|
|
500
|
+
const truncateUtf8 = (text: string, maxBytes: number): string => {
|
|
501
|
+
const encoder = new TextEncoder();
|
|
502
|
+
if (encoder.encode(text).length <= maxBytes) return text;
|
|
503
|
+
let bytes = encoder.encode('…').length;
|
|
504
|
+
let kept = '';
|
|
505
|
+
for (const character of text) {
|
|
506
|
+
bytes += encoder.encode(character).length;
|
|
507
|
+
if (bytes > maxBytes) break;
|
|
508
|
+
kept += character;
|
|
509
|
+
}
|
|
510
|
+
return `${kept}…`;
|
|
511
|
+
};
|
|
512
|
+
|
|
343
513
|
const createExposedToolName = (
|
|
344
514
|
serverName: string,
|
|
345
515
|
toolName: string,
|
package/ts_mcp/plugins.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol
|
|
|
10
10
|
export type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
11
11
|
export type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
12
12
|
|
|
13
|
+
// @modelprofile.com scope
|
|
14
|
+
import { FLEX_TOOL_CATALOG_LIMITS, FlexToolCatalog } from '@modelprofile.com/flexharness';
|
|
15
|
+
|
|
16
|
+
export { FLEX_TOOL_CATALOG_LIMITS, FlexToolCatalog };
|
|
17
|
+
export type { IFlexCatalogMethod, IFlexCatalogRunContext, IFlexToolCatalog } from '@modelprofile.com/flexharness';
|
|
18
|
+
|
|
13
19
|
// ai-sdk core
|
|
14
20
|
import { jsonSchema, tool } from 'ai';
|
|
15
21
|
|
package/ts_mcp/readme.md
CHANGED
|
@@ -8,6 +8,14 @@ the agent and harness do not install or register vendor SDKs.
|
|
|
8
8
|
pnpm add @modelprofile.com/flexharness @modelcontextprotocol/sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`createMcpTools()` connects MCP servers and exposes their tools as AI SDK tools.
|
|
12
|
+
`createMcpToolCatalog()` makes the tools servers list into a FlexHarness tool catalog
|
|
13
|
+
(`search_tools`, `describe_tools`, `execute`, `run_code`): one catalog object for every
|
|
14
|
+
run, each call on the client `resolveClient` names for its run, method paths equal to
|
|
15
|
+
`createMcpTools()`' tool names, `authorizeToolCall` deciding catalog calls as it decides
|
|
16
|
+
tool calls, and read-only methods only where `isReadOnly` declares them. The root readme's
|
|
17
|
+
Tool Catalog section shows a tool provider that connects clients per run.
|
|
18
|
+
|
|
11
19
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
12
20
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
13
21
|
|