@modelprofile.com/flexharness 6.1.0 → 7.0.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.
Files changed (169) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts_chat/classes.chatsession.d.ts +37 -0
  3. package/dist_ts_chat/classes.chatsession.js +102 -0
  4. package/dist_ts_chat/index.d.ts +2 -0
  5. package/dist_ts_chat/index.js +2 -0
  6. package/dist_ts_chat/interfaces.d.ts +72 -0
  7. package/dist_ts_chat/interfaces.js +2 -0
  8. package/dist_ts_chat/plugins.d.ts +4 -0
  9. package/dist_ts_chat/plugins.js +4 -0
  10. package/dist_ts_chat_cli/components.chatapp.d.ts +10 -0
  11. package/dist_ts_chat_cli/components.chatapp.js +75 -0
  12. package/dist_ts_chat_cli/components.inputarea.d.ts +7 -0
  13. package/dist_ts_chat_cli/components.inputarea.js +18 -0
  14. package/dist_ts_chat_cli/components.message.d.ts +12 -0
  15. package/dist_ts_chat_cli/components.message.js +21 -0
  16. package/dist_ts_chat_cli/components.messagelist.d.ts +9 -0
  17. package/dist_ts_chat_cli/components.messagelist.js +19 -0
  18. package/dist_ts_chat_cli/components.statusbar.d.ts +9 -0
  19. package/dist_ts_chat_cli/components.statusbar.js +15 -0
  20. package/dist_ts_chat_cli/index.d.ts +2 -0
  21. package/dist_ts_chat_cli/index.js +2 -0
  22. package/dist_ts_chat_cli/plugins.d.ts +12 -0
  23. package/dist_ts_chat_cli/plugins.js +14 -0
  24. package/dist_ts_chat_cli/startchat.d.ts +18 -0
  25. package/dist_ts_chat_cli/startchat.js +17 -0
  26. package/dist_ts_chat_web/flexchat-input.d.ts +21 -0
  27. package/dist_ts_chat_web/flexchat-input.js +172 -0
  28. package/dist_ts_chat_web/flexchat-message.d.ts +26 -0
  29. package/dist_ts_chat_web/flexchat-message.js +174 -0
  30. package/dist_ts_chat_web/flexchat-window.d.ts +34 -0
  31. package/dist_ts_chat_web/flexchat-window.js +394 -0
  32. package/dist_ts_chat_web/index.d.ts +3 -0
  33. package/dist_ts_chat_web/index.js +4 -0
  34. package/dist_ts_chat_web/plugins.d.ts +6 -0
  35. package/dist_ts_chat_web/plugins.js +4 -0
  36. package/dist_ts_compaction/index.d.ts +2 -0
  37. package/dist_ts_compaction/index.js +2 -0
  38. package/dist_ts_compaction/plugins.d.ts +4 -0
  39. package/dist_ts_compaction/plugins.js +3 -0
  40. package/dist_ts_compaction/smartagent.compaction.d.ts +14 -0
  41. package/dist_ts_compaction/smartagent.compaction.js +49 -0
  42. package/dist_ts_document/index.d.ts +19 -0
  43. package/dist_ts_document/index.js +55 -0
  44. package/dist_ts_document/plugins.d.ts +3 -0
  45. package/dist_ts_document/plugins.js +4 -0
  46. package/dist_ts_kvm/classes.browserkvm.d.ts +22 -0
  47. package/dist_ts_kvm/classes.browserkvm.js +271 -0
  48. package/dist_ts_kvm/classes.kvmterminal.d.ts +8 -0
  49. package/dist_ts_kvm/classes.kvmterminal.js +88 -0
  50. package/dist_ts_kvm/commandwrappers.d.ts +14 -0
  51. package/dist_ts_kvm/commandwrappers.js +58 -0
  52. package/dist_ts_kvm/index.d.ts +6 -0
  53. package/dist_ts_kvm/index.js +7 -0
  54. package/dist_ts_kvm/interfaces.d.ts +106 -0
  55. package/dist_ts_kvm/interfaces.js +2 -0
  56. package/dist_ts_kvm/ocr.d.ts +14 -0
  57. package/dist_ts_kvm/ocr.js +23 -0
  58. package/dist_ts_kvm/plugins.d.ts +6 -0
  59. package/dist_ts_kvm/plugins.js +10 -0
  60. package/dist_ts_kvm/tools.d.ts +15 -0
  61. package/dist_ts_kvm/tools.js +40 -0
  62. package/dist_ts_mcp/index.d.ts +86 -0
  63. package/dist_ts_mcp/index.js +284 -0
  64. package/dist_ts_mcp/requestcontext.d.ts +11 -0
  65. package/dist_ts_mcp/requestcontext.js +42 -0
  66. package/dist_ts_media/index.d.ts +8 -0
  67. package/dist_ts_media/index.js +21 -0
  68. package/dist_ts_media/plugins.d.ts +2 -0
  69. package/dist_ts_media/plugins.js +3 -0
  70. package/dist_ts_ocr/classes.imageocr.d.ts +19 -0
  71. package/dist_ts_ocr/classes.imageocr.js +27 -0
  72. package/dist_ts_ocr/index.d.ts +2 -0
  73. package/dist_ts_ocr/index.js +3 -0
  74. package/dist_ts_ocr/ocr.mistral.d.ts +75 -0
  75. package/dist_ts_ocr/ocr.mistral.js +77 -0
  76. package/dist_ts_tools/index.d.ts +17 -0
  77. package/dist_ts_tools/index.js +10 -0
  78. package/dist_ts_tools/plugins.d.ts +7 -0
  79. package/dist_ts_tools/plugins.js +6 -0
  80. package/dist_ts_tools/tool.browser.d.ts +11 -0
  81. package/dist_ts_tools/tool.browser.js +128 -0
  82. package/dist_ts_tools/tool.context.d.ts +4 -0
  83. package/dist_ts_tools/tool.context.js +23 -0
  84. package/dist_ts_tools/tool.filesystem.d.ts +11 -0
  85. package/dist_ts_tools/tool.filesystem.js +106 -0
  86. package/dist_ts_tools/tool.http.d.ts +2 -0
  87. package/dist_ts_tools/tool.http.js +65 -0
  88. package/dist_ts_tools/tool.json.d.ts +2 -0
  89. package/dist_ts_tools/tool.json.js +47 -0
  90. package/dist_ts_tools/tool.shell.d.ts +15 -0
  91. package/dist_ts_tools/tool.shell.js +329 -0
  92. package/dist_ts_tools/utils.truncation.d.ts +10 -0
  93. package/dist_ts_tools/utils.truncation.js +26 -0
  94. package/dist_ts_tools_node/index.d.ts +5 -0
  95. package/dist_ts_tools_node/index.js +4 -0
  96. package/dist_ts_tools_node/plugins.d.ts +13 -0
  97. package/dist_ts_tools_node/plugins.js +13 -0
  98. package/dist_ts_tools_node/tool.context.d.ts +31 -0
  99. package/dist_ts_tools_node/tool.context.js +837 -0
  100. package/dist_ts_tools_node/tool.persistence.d.ts +15 -0
  101. package/dist_ts_tools_node/tool.persistence.js +129 -0
  102. package/dist_ts_tools_node/tools.local.d.ts +11 -0
  103. package/dist_ts_tools_node/tools.local.js +15 -0
  104. package/package.json +112 -4
  105. package/readme.md +122 -66
  106. package/ts/00_commitinfo_data.ts +1 -1
  107. package/ts/readme.md +122 -66
  108. package/ts_chat/classes.chatsession.ts +116 -0
  109. package/ts_chat/index.ts +8 -0
  110. package/ts_chat/interfaces.ts +64 -0
  111. package/ts_chat/plugins.ts +7 -0
  112. package/ts_chat/readme.md +49 -0
  113. package/ts_chat_cli/components.chatapp.ts +103 -0
  114. package/ts_chat_cli/components.inputarea.ts +29 -0
  115. package/ts_chat_cli/components.message.ts +48 -0
  116. package/ts_chat_cli/components.messagelist.ts +45 -0
  117. package/ts_chat_cli/components.statusbar.ts +31 -0
  118. package/ts_chat_cli/index.ts +2 -0
  119. package/ts_chat_cli/plugins.ts +21 -0
  120. package/ts_chat_cli/readme.md +40 -0
  121. package/ts_chat_cli/startchat.ts +34 -0
  122. package/ts_chat_web/flexchat-input.ts +184 -0
  123. package/ts_chat_web/flexchat-message.ts +185 -0
  124. package/ts_chat_web/flexchat-window.ts +416 -0
  125. package/ts_chat_web/index.ts +3 -0
  126. package/ts_chat_web/plugins.ts +9 -0
  127. package/ts_chat_web/readme.md +46 -0
  128. package/ts_compaction/index.ts +2 -0
  129. package/ts_compaction/plugins.ts +6 -0
  130. package/ts_compaction/readme.md +37 -0
  131. package/ts_compaction/smartagent.compaction.ts +59 -0
  132. package/ts_document/index.ts +82 -0
  133. package/ts_document/plugins.ts +3 -0
  134. package/ts_document/readme.md +49 -0
  135. package/ts_kvm/classes.browserkvm.ts +335 -0
  136. package/ts_kvm/classes.kvmterminal.ts +103 -0
  137. package/ts_kvm/commandwrappers.ts +83 -0
  138. package/ts_kvm/index.ts +6 -0
  139. package/ts_kvm/interfaces.ts +152 -0
  140. package/ts_kvm/ocr.ts +41 -0
  141. package/ts_kvm/plugins.ts +13 -0
  142. package/ts_kvm/readme.md +482 -0
  143. package/ts_kvm/tools.ts +58 -0
  144. package/ts_mcp/index.ts +470 -0
  145. package/ts_mcp/readme.md +37 -0
  146. package/ts_mcp/requestcontext.ts +66 -0
  147. package/ts_media/index.ts +29 -0
  148. package/ts_media/plugins.ts +2 -0
  149. package/ts_media/readme.md +37 -0
  150. package/ts_ocr/classes.imageocr.ts +53 -0
  151. package/ts_ocr/index.ts +2 -0
  152. package/ts_ocr/ocr.mistral.ts +192 -0
  153. package/ts_ocr/readme.md +48 -0
  154. package/ts_tools/index.ts +49 -0
  155. package/ts_tools/plugins.ts +7 -0
  156. package/ts_tools/readme.md +37 -0
  157. package/ts_tools/tool.browser.ts +159 -0
  158. package/ts_tools/tool.context.ts +23 -0
  159. package/ts_tools/tool.filesystem.ts +144 -0
  160. package/ts_tools/tool.http.ts +78 -0
  161. package/ts_tools/tool.json.ts +53 -0
  162. package/ts_tools/tool.shell.ts +418 -0
  163. package/ts_tools/utils.truncation.ts +39 -0
  164. package/ts_tools_node/index.ts +5 -0
  165. package/ts_tools_node/plugins.ts +16 -0
  166. package/ts_tools_node/readme.md +37 -0
  167. package/ts_tools_node/tool.context.ts +991 -0
  168. package/ts_tools_node/tool.persistence.ts +146 -0
  169. package/ts_tools_node/tools.local.ts +26 -0
@@ -0,0 +1,58 @@
1
+ import { KvmTerminal } from './classes.kvmterminal.js';
2
+ import type { IKvmTerminalCommandResult } from './interfaces.js';
3
+
4
+ export interface IKvmTool<TInput = unknown, TOutput = unknown> {
5
+ name: string;
6
+ description: string;
7
+ parameters: unknown;
8
+ execute: (input: TInput) => Promise<TOutput>;
9
+ }
10
+
11
+ export interface IKvmToolOptions {
12
+ terminal: KvmTerminal;
13
+ }
14
+
15
+ export interface IKvmTerminalRunCommandInput {
16
+ command: string;
17
+ }
18
+
19
+ export type TKvmTerminalObserveInput = Record<string, never>;
20
+
21
+ export const createKvmTools = (options: IKvmToolOptions): IKvmTool[] => {
22
+ return [
23
+ {
24
+ name: 'kvm_terminal_run_command',
25
+ description: 'Run a command through the visual KVM terminal transport and return OCR-parsed output.',
26
+ parameters: {
27
+ type: 'object',
28
+ properties: {
29
+ command: {
30
+ type: 'string',
31
+ description: 'The terminal command to run.',
32
+ },
33
+ },
34
+ required: ['command'],
35
+ additionalProperties: false,
36
+ },
37
+ execute: async (input: unknown): Promise<IKvmTerminalCommandResult> => {
38
+ const typedInput = input as Partial<IKvmTerminalRunCommandInput>;
39
+ if (!typedInput || typeof typedInput.command !== 'string') {
40
+ throw new Error('kvm_terminal_run_command requires a string command.');
41
+ }
42
+ return options.terminal.runCommand(typedInput.command);
43
+ },
44
+ },
45
+ {
46
+ name: 'kvm_terminal_observe',
47
+ description: 'Observe the current visual KVM terminal text through the configured OCR engine.',
48
+ parameters: {
49
+ type: 'object',
50
+ properties: {},
51
+ additionalProperties: false,
52
+ },
53
+ execute: async (): Promise<string> => {
54
+ return options.terminal.observeText();
55
+ },
56
+ },
57
+ ];
58
+ };
@@ -0,0 +1,470 @@
1
+ import { Client, type ClientOptions } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import {
3
+ StdioClientTransport,
4
+ type StdioServerParameters,
5
+ } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ import {
7
+ StreamableHTTPClientTransport,
8
+ type StreamableHTTPClientTransportOptions,
9
+ } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
10
+ import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
11
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
12
+ import { jsonSchema, tool } from 'ai';
13
+ import type { ToolSet } from 'ai';
14
+ import type { ToolExecutionOptions } from 'ai';
15
+ import {
16
+ connectMcpClientWithRequestOptions,
17
+ withMcpRequestOptions,
18
+ } from './requestcontext.js';
19
+
20
+ const DEFAULT_MAX_LIST_TOOLS_PAGES = 100;
21
+
22
+ export type TMcpListToolsResult = Awaited<ReturnType<Client['listTools']>>;
23
+ export type TMcpToolMetadata = TMcpListToolsResult['tools'][number];
24
+ export type TMcpCallToolResult = Awaited<ReturnType<Client['callTool']>>;
25
+
26
+ export interface IMcpClientLike {
27
+ listTools(params?: { cursor?: string }, options?: RequestOptions): Promise<TMcpListToolsResult>;
28
+ callTool(
29
+ params: { name: string; arguments?: Record<string, unknown> },
30
+ resultSchema?: unknown,
31
+ options?: RequestOptions,
32
+ ): Promise<TMcpCallToolResult>;
33
+ close?(): Promise<void>;
34
+ }
35
+
36
+ export interface IMcpServerClientOptions {
37
+ clientName?: string;
38
+ clientVersion?: string;
39
+ clientOptions?: ClientOptions;
40
+ requestOptions?: RequestOptions;
41
+ }
42
+
43
+ export interface IMcpStdioServerConfig extends StdioServerParameters, IMcpServerClientOptions {
44
+ type: 'stdio';
45
+ }
46
+
47
+ export interface IMcpStreamableHttpServerConfig extends IMcpServerClientOptions {
48
+ type: 'streamableHttp' | 'http';
49
+ url: string | URL;
50
+ transportOptions?: StreamableHTTPClientTransportOptions;
51
+ }
52
+
53
+ export interface IMcpTransportServerConfig extends IMcpServerClientOptions {
54
+ type: 'transport';
55
+ transport: Transport;
56
+ }
57
+
58
+ export interface IMcpClientServerConfig {
59
+ type: 'client';
60
+ client: IMcpClientLike;
61
+ }
62
+
63
+ export type TMcpServerConfig =
64
+ | IMcpStdioServerConfig
65
+ | IMcpStreamableHttpServerConfig
66
+ | IMcpTransportServerConfig
67
+ | IMcpClientServerConfig;
68
+
69
+ export interface IMcpToolFormatContext {
70
+ serverName: string;
71
+ toolName: string;
72
+ exposedToolName: string;
73
+ }
74
+
75
+ export interface IMcpAuthorizeToolCallContext extends IMcpToolFormatContext {
76
+ arguments: Record<string, unknown>;
77
+ tool: TMcpToolMetadata;
78
+ toolCallId: string;
79
+ abortSignal?: AbortSignal;
80
+ }
81
+
82
+ export interface ICreateMcpToolsOptions {
83
+ servers: Record<string, TMcpServerConfig>;
84
+ clientName?: string;
85
+ clientVersion?: string;
86
+ clientOptions?: ClientOptions;
87
+ requestOptions?: RequestOptions;
88
+ listToolsRequestOptions?: RequestOptions;
89
+ callToolRequestOptions?: RequestOptions;
90
+ /** Maximum MCP listTools pages per server. Default: 100. */
91
+ maxListToolsPages?: number;
92
+ prefixToolNames?: boolean;
93
+ formatResult?: (result: TMcpCallToolResult, context: IMcpToolFormatContext) => unknown;
94
+ /** Resolve to allow the MCP tool call, or throw to deny it. Omission allows all calls. */
95
+ authorizeToolCall?: (context: IMcpAuthorizeToolCallContext) => PromiseLike<void> | void;
96
+ }
97
+
98
+ export interface IMcpConnectedServer {
99
+ name: string;
100
+ client: IMcpClientLike;
101
+ tools: TMcpToolMetadata[];
102
+ }
103
+
104
+ export interface IMcpToolNameMapping {
105
+ serverName: string;
106
+ toolName: string;
107
+ }
108
+
109
+ export interface ICreateMcpToolsResult {
110
+ tools: ToolSet;
111
+ servers: IMcpConnectedServer[];
112
+ toolNameMap: Record<string, IMcpToolNameMapping>;
113
+ close: () => Promise<void>;
114
+ }
115
+
116
+ export const createMcpTools = async (
117
+ options: ICreateMcpToolsOptions,
118
+ ): Promise<ICreateMcpToolsResult> => {
119
+ const serverEntries = Object.entries(options.servers);
120
+ if (serverEntries.length === 0) {
121
+ throw new Error('createMcpTools requires at least one MCP server.');
122
+ }
123
+ const maxListToolsPages = normalizeMaxListToolsPages(options.maxListToolsPages);
124
+
125
+ const connectedServers: IMcpConnectedServer[] = [];
126
+ try {
127
+ for (const [serverName, config] of serverEntries) {
128
+ const client = await connectMcpServer(serverName, config, options);
129
+ const connectedServer: IMcpConnectedServer = { name: serverName, client, tools: [] };
130
+ connectedServers.push(connectedServer);
131
+ connectedServer.tools = await listAllMcpTools(
132
+ client,
133
+ options.listToolsRequestOptions ?? getServerRequestOptions(config) ?? options.requestOptions,
134
+ maxListToolsPages,
135
+ );
136
+ }
137
+
138
+ const exposedTools: ToolSet = {};
139
+ const toolNameMap: Record<string, IMcpToolNameMapping> = {};
140
+ const usedToolNames = new Set<string>(['invalid']);
141
+ const formatResult = options.formatResult ?? formatMcpToolResult;
142
+ const shouldPrefixToolNames = options.prefixToolNames ?? connectedServers.length > 1;
143
+
144
+ for (const server of connectedServers) {
145
+ for (const mcpTool of server.tools) {
146
+ const baseToolName = createExposedToolName(server.name, mcpTool.name, shouldPrefixToolNames);
147
+ const exposedToolName = createUniqueToolName(baseToolName, usedToolNames);
148
+ usedToolNames.add(exposedToolName);
149
+ toolNameMap[exposedToolName] = {
150
+ serverName: server.name,
151
+ toolName: mcpTool.name,
152
+ };
153
+
154
+ exposedTools[exposedToolName] = tool({
155
+ description: mcpTool.description ?? `MCP tool ${mcpTool.name} from ${server.name}`,
156
+ inputSchema: jsonSchema(mcpTool.inputSchema as never),
157
+ execute: async (input: unknown, invocationOptions?: ToolExecutionOptions<unknown>) => {
158
+ const normalizedArguments = normalizeMcpArguments(input);
159
+ await options.authorizeToolCall?.({
160
+ serverName: server.name,
161
+ toolName: mcpTool.name,
162
+ exposedToolName,
163
+ arguments: normalizedArguments,
164
+ tool: mcpTool,
165
+ toolCallId: invocationOptions?.toolCallId ?? '',
166
+ abortSignal: invocationOptions?.abortSignal,
167
+ });
168
+ const configuredCallToolOptions: RequestOptions = {
169
+ ...options.requestOptions,
170
+ ...options.callToolRequestOptions,
171
+ };
172
+ const result = await withMcpRequestOptions(
173
+ configuredCallToolOptions,
174
+ invocationOptions?.abortSignal,
175
+ (requestOptions) => server.client.callTool(
176
+ {
177
+ name: mcpTool.name,
178
+ arguments: normalizedArguments,
179
+ },
180
+ undefined,
181
+ requestOptions,
182
+ ),
183
+ );
184
+ const formattedResult = formatResult(result, {
185
+ serverName: server.name,
186
+ toolName: mcpTool.name,
187
+ exposedToolName,
188
+ });
189
+ if ((result as Record<string, unknown>).isError === true) {
190
+ throw new Error(formatMcpError(formattedResult));
191
+ }
192
+ return formattedResult;
193
+ },
194
+ });
195
+ }
196
+ }
197
+
198
+ return {
199
+ tools: exposedTools,
200
+ servers: connectedServers,
201
+ toolNameMap,
202
+ close: async () => closeMcpClients(connectedServers),
203
+ };
204
+ } catch (error) {
205
+ return rethrowAfterCleanup(
206
+ error,
207
+ () => closeMcpClients(connectedServers),
208
+ 'Failed to create MCP tools and clean up connected clients.',
209
+ );
210
+ }
211
+ };
212
+
213
+ export const sanitizeMcpToolName = (name: string, fallback = 'mcp_tool'): string => {
214
+ const sanitized = name
215
+ .trim()
216
+ .replace(/[^a-zA-Z0-9_-]+/g, '_')
217
+ .replace(/^_+|_+$/g, '');
218
+ const safeName = sanitized || fallback;
219
+ return /^[a-zA-Z_]/.test(safeName) ? safeName : `_${safeName}`;
220
+ };
221
+
222
+ export const formatMcpToolResult = (result: TMcpCallToolResult): unknown => {
223
+ const resultRecord = result as Record<string, unknown>;
224
+ if (Object.prototype.hasOwnProperty.call(resultRecord, 'toolResult')) {
225
+ return resultRecord.toolResult;
226
+ }
227
+
228
+ const content = Array.isArray(resultRecord.content)
229
+ ? resultRecord.content.map(formatMcpContentPart).filter(Boolean).join('\n\n')
230
+ : '';
231
+ const formattedContent = resultRecord.isError && content
232
+ ? `MCP tool returned an error:\n${content}`
233
+ : content;
234
+ const structuredContent = resultRecord.structuredContent;
235
+
236
+ if (structuredContent !== undefined && formattedContent) {
237
+ return `${formattedContent}\n\nStructured content:\n${stringifyJson(structuredContent)}`;
238
+ }
239
+ if (structuredContent !== undefined) {
240
+ return structuredContent;
241
+ }
242
+ if (formattedContent) {
243
+ return formattedContent;
244
+ }
245
+ return stringifyJson(result);
246
+ };
247
+
248
+ const connectMcpServer = async (
249
+ serverName: string,
250
+ config: TMcpServerConfig,
251
+ options: ICreateMcpToolsOptions,
252
+ ): Promise<IMcpClientLike> => {
253
+ if (config.type === 'client') {
254
+ return config.client;
255
+ }
256
+
257
+ const client = new Client(
258
+ {
259
+ name: config.clientName ?? options.clientName ?? 'smartagent-mcp',
260
+ version: config.clientVersion ?? options.clientVersion ?? '1.0.0',
261
+ },
262
+ config.clientOptions ?? options.clientOptions ?? { capabilities: {} },
263
+ );
264
+ const transport = createMcpTransport(config);
265
+ try {
266
+ await connectMcpClientWithRequestOptions(
267
+ client,
268
+ transport,
269
+ config.requestOptions ?? options.requestOptions,
270
+ );
271
+ } catch (error) {
272
+ await rethrowAfterCleanup(
273
+ error,
274
+ async () => {
275
+ try {
276
+ await client.close();
277
+ } catch (clientCloseError) {
278
+ try {
279
+ await transport.close();
280
+ } catch (transportCloseError) {
281
+ throw new AggregateError(
282
+ [clientCloseError, transportCloseError],
283
+ `Failed to close MCP client and transport for "${serverName}".`,
284
+ );
285
+ }
286
+ throw clientCloseError;
287
+ }
288
+ },
289
+ `Failed to connect MCP server "${serverName}" and clean up its client.`,
290
+ );
291
+ }
292
+ return client;
293
+ };
294
+
295
+ const createMcpTransport = (
296
+ config: Exclude<TMcpServerConfig, IMcpClientServerConfig>,
297
+ ): Transport => {
298
+ if (config.type === 'stdio') {
299
+ const { type, clientName, clientVersion, clientOptions, requestOptions, ...serverParameters } = config;
300
+ return new StdioClientTransport(serverParameters);
301
+ }
302
+
303
+ if (config.type === 'streamableHttp' || config.type === 'http') {
304
+ return new StreamableHTTPClientTransport(new URL(config.url), config.transportOptions);
305
+ }
306
+
307
+ if (config.type === 'transport') {
308
+ return config.transport;
309
+ }
310
+
311
+ throw new Error('Unsupported MCP server transport.');
312
+ };
313
+
314
+ const getServerRequestOptions = (config: TMcpServerConfig): RequestOptions | undefined => {
315
+ return config.type === 'client' ? undefined : config.requestOptions;
316
+ };
317
+
318
+ const listAllMcpTools = async (
319
+ client: IMcpClientLike,
320
+ requestOptions?: RequestOptions,
321
+ maxPages = DEFAULT_MAX_LIST_TOOLS_PAGES,
322
+ ): Promise<TMcpToolMetadata[]> => {
323
+ const tools: TMcpToolMetadata[] = [];
324
+ const seenCursors = new Set<string>();
325
+ let cursor: string | undefined;
326
+ for (let page = 1; page <= maxPages; page++) {
327
+ const result = await withMcpRequestOptions(
328
+ requestOptions,
329
+ undefined,
330
+ (isolatedRequestOptions) => client.listTools(
331
+ cursor ? { cursor } : undefined,
332
+ isolatedRequestOptions,
333
+ ),
334
+ );
335
+ tools.push(...result.tools);
336
+ const nextCursor = result.nextCursor;
337
+ if (!nextCursor) {
338
+ return tools;
339
+ }
340
+ if (seenCursors.has(nextCursor)) {
341
+ throw new Error(`MCP listTools returned repeated cursor "${nextCursor}".`);
342
+ }
343
+ seenCursors.add(nextCursor);
344
+ cursor = nextCursor;
345
+ }
346
+ throw new Error(`MCP listTools exceeded the maximum of ${maxPages} pages.`);
347
+ };
348
+
349
+ const normalizeMaxListToolsPages = (maxPages = DEFAULT_MAX_LIST_TOOLS_PAGES): number => {
350
+ if (!Number.isSafeInteger(maxPages) || maxPages <= 0) {
351
+ throw new Error('maxListToolsPages must be a positive integer.');
352
+ }
353
+ return maxPages;
354
+ };
355
+
356
+ const createExposedToolName = (
357
+ serverName: string,
358
+ toolName: string,
359
+ prefixToolName: boolean,
360
+ ): string => {
361
+ const safeToolName = sanitizeMcpToolName(toolName);
362
+ if (!prefixToolName) {
363
+ return safeToolName;
364
+ }
365
+ return `${sanitizeMcpToolName(serverName, 'mcp')}__${safeToolName}`;
366
+ };
367
+
368
+ const createUniqueToolName = (baseToolName: string, usedToolNames: Set<string>): string => {
369
+ if (!usedToolNames.has(baseToolName)) {
370
+ return baseToolName;
371
+ }
372
+ let suffix = 2;
373
+ while (usedToolNames.has(`${baseToolName}_${suffix}`)) {
374
+ suffix++;
375
+ }
376
+ return `${baseToolName}_${suffix}`;
377
+ };
378
+
379
+ const normalizeMcpArguments = (input: unknown): Record<string, unknown> => {
380
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
381
+ return {};
382
+ }
383
+ return input as Record<string, unknown>;
384
+ };
385
+
386
+ const formatMcpError = (formattedResult: unknown): string => {
387
+ if (typeof formattedResult === 'string' && formattedResult) {
388
+ return formattedResult;
389
+ }
390
+ const formatted = stringifyJson(formattedResult);
391
+ return formatted || 'MCP tool returned an error.';
392
+ };
393
+
394
+ const formatMcpContentPart = (part: unknown): string => {
395
+ if (!part || typeof part !== 'object') {
396
+ return stringifyJson(part);
397
+ }
398
+ const partRecord = part as Record<string, unknown>;
399
+ const type = partRecord.type;
400
+ if (type === 'text') {
401
+ return typeof partRecord.text === 'string' ? partRecord.text : '';
402
+ }
403
+ if (type === 'image' || type === 'audio') {
404
+ const data = typeof partRecord.data === 'string' ? partRecord.data : '';
405
+ const mimeType = typeof partRecord.mimeType === 'string' ? partRecord.mimeType : 'application/octet-stream';
406
+ return `[${type}: ${mimeType}; base64 length ${data.length}]`;
407
+ }
408
+ if (type === 'resource') {
409
+ return formatMcpResourcePart(partRecord.resource);
410
+ }
411
+ if (type === 'resource_link') {
412
+ const name = typeof partRecord.name === 'string' ? partRecord.name : 'resource';
413
+ const uri = typeof partRecord.uri === 'string' ? partRecord.uri : '';
414
+ return `Resource link ${name}${uri ? `: ${uri}` : ''}`;
415
+ }
416
+ return stringifyJson(part);
417
+ };
418
+
419
+ const formatMcpResourcePart = (resource: unknown): string => {
420
+ if (!resource || typeof resource !== 'object') {
421
+ return stringifyJson(resource);
422
+ }
423
+ const resourceRecord = resource as Record<string, unknown>;
424
+ const uri = typeof resourceRecord.uri === 'string' ? resourceRecord.uri : 'resource';
425
+ if (typeof resourceRecord.text === 'string') {
426
+ return `Resource ${uri}:\n${resourceRecord.text}`;
427
+ }
428
+ if (typeof resourceRecord.blob === 'string') {
429
+ const mimeType = typeof resourceRecord.mimeType === 'string' ? resourceRecord.mimeType : 'application/octet-stream';
430
+ return `[resource ${uri}: ${mimeType}; base64 length ${resourceRecord.blob.length}]`;
431
+ }
432
+ return stringifyJson(resource);
433
+ };
434
+
435
+ const stringifyJson = (value: unknown): string => {
436
+ if (typeof value === 'string') {
437
+ return value;
438
+ }
439
+ try {
440
+ return JSON.stringify(value, undefined, 2);
441
+ } catch {
442
+ return String(value);
443
+ }
444
+ };
445
+
446
+ const closeMcpClients = async (servers: IMcpConnectedServer[]): Promise<void> => {
447
+ const results = await Promise.allSettled(servers.map((server) => server.client.close?.()));
448
+ const errors = results
449
+ .filter((result): result is PromiseRejectedResult => result.status === 'rejected')
450
+ .map((result) => result.reason);
451
+ if (errors.length > 0) {
452
+ throw new AggregateError(errors, 'Failed to close one or more MCP clients.');
453
+ }
454
+ };
455
+
456
+ const rethrowAfterCleanup = async (
457
+ originalError: unknown,
458
+ cleanup: () => Promise<void>,
459
+ message: string,
460
+ ): Promise<never> => {
461
+ try {
462
+ await cleanup();
463
+ } catch (cleanupError) {
464
+ const cleanupErrors = cleanupError instanceof AggregateError
465
+ ? cleanupError.errors
466
+ : [cleanupError];
467
+ throw new AggregateError([originalError, ...cleanupErrors], message);
468
+ }
469
+ throw originalError;
470
+ };
@@ -0,0 +1,37 @@
1
+ MCP client connections and AI SDK tool conversion with request-scoped cancellation.
2
+
3
+ Part of the [FlexHarness toolbox](https://code.foss.global/modelprofile.com/flexharness).
4
+ Install the components your project uses. Provider adapters are registered explicitly;
5
+ the agent and harness do not install or register vendor SDKs.
6
+
7
+ ```sh
8
+ pnpm add @modelprofile.com/flexharness @modelcontextprotocol/sdk
9
+ ```
10
+
11
+ See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
12
+ for composition examples and the SmartAI/SmartAgent migration map.
13
+
14
+ ## Issue Reporting and Security
15
+
16
+ Report issues and security concerns through [community.foss.global](https://community.foss.global).
17
+
18
+ ## License and Legal Information
19
+
20
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
21
+
22
+ **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
23
+
24
+ ### Trademarks
25
+
26
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
27
+
28
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
29
+
30
+ ### Company Information
31
+
32
+ Task Venture Capital GmbH<br>
33
+ Registered at District Court Bremen HRB 35230 HB, Germany
34
+
35
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
36
+
37
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
@@ -0,0 +1,66 @@
1
+ import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
2
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
3
+
4
+ export interface IMcpRequestContext {
5
+ requestOptions?: RequestOptions;
6
+ cleanup: () => void;
7
+ }
8
+
9
+ export const createMcpRequestContext = (
10
+ configuredOptions?: RequestOptions,
11
+ preferredAbortSignal?: AbortSignal,
12
+ ): IMcpRequestContext => {
13
+ const requestOptions: RequestOptions = { ...configuredOptions };
14
+ const sourceSignal = preferredAbortSignal ?? requestOptions.signal;
15
+ if (!sourceSignal) {
16
+ return {
17
+ requestOptions: Object.keys(requestOptions).length > 0 ? requestOptions : undefined,
18
+ cleanup: () => undefined,
19
+ };
20
+ }
21
+
22
+ const relayController = new AbortController();
23
+ const forwardAbort = () => relayController.abort(sourceSignal.reason);
24
+ let listenerWasAdded = false;
25
+ if (sourceSignal.aborted) {
26
+ forwardAbort();
27
+ } else {
28
+ sourceSignal.addEventListener('abort', forwardAbort, { once: true });
29
+ listenerWasAdded = true;
30
+ }
31
+
32
+ return {
33
+ requestOptions: {
34
+ ...requestOptions,
35
+ signal: relayController.signal,
36
+ },
37
+ cleanup: () => {
38
+ if (listenerWasAdded) {
39
+ sourceSignal.removeEventListener('abort', forwardAbort);
40
+ }
41
+ },
42
+ };
43
+ };
44
+
45
+ export const withMcpRequestOptions = async <T>(
46
+ configuredOptions: RequestOptions | undefined,
47
+ preferredAbortSignal: AbortSignal | undefined,
48
+ request: (requestOptions?: RequestOptions) => Promise<T>,
49
+ ): Promise<T> => {
50
+ const context = createMcpRequestContext(configuredOptions, preferredAbortSignal);
51
+ try {
52
+ return await request(context.requestOptions);
53
+ } finally {
54
+ context.cleanup();
55
+ }
56
+ };
57
+
58
+ export const connectMcpClientWithRequestOptions = async (
59
+ client: { connect: (transport: Transport, requestOptions?: RequestOptions) => Promise<void> },
60
+ transport: Transport,
61
+ configuredOptions?: RequestOptions,
62
+ ): Promise<void> => withMcpRequestOptions(
63
+ configuredOptions,
64
+ undefined,
65
+ (requestOptions) => client.connect(transport, requestOptions),
66
+ );
@@ -0,0 +1,29 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
3
+
4
+ export interface IVisionOptions {
5
+ model: TSmartAiLanguageModel;
6
+ image: Buffer | Uint8Array;
7
+ prompt: string;
8
+ mediaType?: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif';
9
+ }
10
+
11
+ export async function analyzeImage(options: IVisionOptions): Promise<string> {
12
+ const result = await plugins.generateText({
13
+ model: options.model,
14
+ messages: [
15
+ {
16
+ role: 'user',
17
+ content: [
18
+ { type: 'text', text: options.prompt },
19
+ {
20
+ type: 'file',
21
+ data: options.image,
22
+ mediaType: options.mediaType ?? 'image/jpeg',
23
+ },
24
+ ],
25
+ },
26
+ ],
27
+ });
28
+ return result.text;
29
+ }
@@ -0,0 +1,2 @@
1
+ import { generateText } from 'ai';
2
+ export { generateText };