Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b9a17c882 | |||
| 9957485281 | |||
| cf45add905 | |||
| 7b4184e88d |
@@ -7,6 +7,25 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 2026-05-18 - 3.7.0
|
||||
|
||||
### Features
|
||||
|
||||
- add MCP tool integration via dedicated subpath export (mcp)
|
||||
- introduces a new @push.rocks/smartagent/mcp entrypoint with MCP client, stdio, HTTP, and custom transport support
|
||||
- adds MCP tool discovery, name sanitization, multi-server prefixing, result formatting, and cleanup handling
|
||||
- documents MCP usage in the README and adds test coverage for tool exposure, pagination, and result formatting
|
||||
|
||||
## 2026-05-15 - 3.6.0
|
||||
|
||||
### Features
|
||||
|
||||
- document provider prompt caching behavior and SmartAI cache integration (readme)
|
||||
- Adds provider prompt caching to the feature list in the README
|
||||
- Clarifies that provider-specific cache metadata is centralized in @push.rocks/smartai
|
||||
|
||||
## 2026-05-15 - 3.5.0
|
||||
|
||||
### Features
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartagent",
|
||||
"version": "3.5.0",
|
||||
"version": "3.7.0",
|
||||
"private": false,
|
||||
"description": "Agentic loop for ai-sdk (Vercel AI SDK). Wraps streamText with stopWhen for parallel multi-step tool execution. Built on @push.rocks/smartai.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -18,6 +18,10 @@
|
||||
"./compaction": {
|
||||
"import": "./dist_ts_compaction/index.js",
|
||||
"types": "./dist_ts_compaction/index.d.ts"
|
||||
},
|
||||
"./mcp": {
|
||||
"import": "./dist_ts_mcp/index.js",
|
||||
"types": "./dist_ts_mcp/index.d.ts"
|
||||
}
|
||||
},
|
||||
"author": "Task Venture Capital GmbH",
|
||||
@@ -37,6 +41,7 @@
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@push.rocks/smartai": "^4.0.0",
|
||||
"@push.rocks/smartfs": "^1.5.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
@@ -57,6 +62,7 @@
|
||||
"ts/**/*",
|
||||
"ts_tools/**/*",
|
||||
"ts_compaction/**/*",
|
||||
"ts_mcp/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"assets/**/*",
|
||||
|
||||
Generated
+561
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,7 @@ console.log(result.usage); // { inputTokens, outputTokens, totalTokens, cacheR
|
||||
- ⚡ **Parallel tool execution** — multiple tool calls in a single step are executed concurrently
|
||||
- 🔧 **Auto-retry with backoff** — handles 429/529/503 errors with header-aware retry delays
|
||||
- 🩹 **Tool call repair** — case-insensitive name matching + invalid tool sink prevents crashes
|
||||
- 💰 **Provider prompt caching** — uses SmartAI cache helpers for Anthropic breakpoints and OpenAI cache affinity
|
||||
- 📊 **Token and reasoning streaming** — `onToken`, `onReasoning*`, and `onToolCall` callbacks for real-time progress
|
||||
- 💥 **Context overflow handling** — detects overflow and invokes your `onContextOverflow` callback
|
||||
|
||||
@@ -168,6 +169,7 @@ const saved = result.toolCalls.some((call) =>
|
||||
|
||||
SmartAgent enables prompt-cache defaults by default:
|
||||
|
||||
- Cache behavior is provided by `@push.rocks/smartai`, so provider-specific cache metadata stays centralized there.
|
||||
- Anthropic-compatible models get cache breakpoints on the first two system messages and the two most recent non-system messages.
|
||||
- OpenAI models get `store: false` by default and, when `sessionId` is provided, `promptCacheKey: sessionId` with `promptCacheRetention: 'in_memory'`.
|
||||
- Longer retention is opt-in. Use `cache: { retention: '24h' }` for OpenAI or `cache: { retention: '1h' }` for Anthropic.
|
||||
@@ -242,6 +244,40 @@ await runAgent({
|
||||
});
|
||||
```
|
||||
|
||||
## MCP Tools
|
||||
|
||||
MCP support lives in the `@push.rocks/smartagent/mcp` subpath. The main `@push.rocks/smartagent` import stays unchanged unless you opt in.
|
||||
|
||||
```typescript
|
||||
import { runAgent } from '@push.rocks/smartagent';
|
||||
import { createMcpTools } from '@push.rocks/smartagent/mcp';
|
||||
|
||||
const mcp = await createMcpTools({
|
||||
servers: {
|
||||
filesystem: {
|
||||
type: 'stdio',
|
||||
command: 'my-mcp-filesystem-server',
|
||||
args: ['/workspace/project'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runAgent({
|
||||
model,
|
||||
prompt: 'Use the available MCP tools to inspect the project.',
|
||||
tools: mcp.tools,
|
||||
maxSteps: 10,
|
||||
});
|
||||
|
||||
console.log(result.text);
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
```
|
||||
|
||||
`createMcpTools()` supports `stdio`, Streamable HTTP, custom transports, and pre-created MCP clients. When multiple servers are configured, exposed tool names are prefixed with the sanitized server name, for example `filesystem__read_file`. The returned `toolNameMap` maps exposed AI SDK tool names back to their original MCP server/tool names.
|
||||
|
||||
## Reusable Tool Contexts
|
||||
|
||||
SmartAgent can build tools once and execute them through a host-provided context. The same shell, filesystem, and browser tool schemas can target local Node.js, SSH, MCP, or another transport supplied by the host app.
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
createMcpTools,
|
||||
formatMcpToolResult,
|
||||
sanitizeMcpToolName,
|
||||
type IMcpClientLike,
|
||||
} from '../ts_mcp/index.js';
|
||||
|
||||
interface IFakeMcpClientState {
|
||||
calls: Array<{ name: string; arguments?: Record<string, unknown> }>;
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
const createFakeMcpClient = (
|
||||
toolName: string,
|
||||
responsePrefix: string,
|
||||
state: IFakeMcpClientState,
|
||||
): IMcpClientLike => ({
|
||||
listTools: async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: toolName,
|
||||
description: `Runs ${toolName}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: { type: 'string' },
|
||||
},
|
||||
required: ['value'],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any),
|
||||
callTool: async (params) => {
|
||||
state.calls.push(params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `${responsePrefix}: ${String(params.arguments?.value ?? '')}`,
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
ok: true,
|
||||
},
|
||||
} as any;
|
||||
},
|
||||
close: async () => {
|
||||
state.closed = true;
|
||||
},
|
||||
});
|
||||
|
||||
tap.test('sanitizeMcpToolName should produce AI SDK-safe tool names', async () => {
|
||||
expect(sanitizeMcpToolName('read file/path')).toEqual('read_file_path');
|
||||
expect(sanitizeMcpToolName('123-start')).toEqual('_123-start');
|
||||
expect(sanitizeMcpToolName('***', 'fallback')).toEqual('fallback');
|
||||
});
|
||||
|
||||
tap.test('createMcpTools should expose prefixed tools for multiple MCP servers', async () => {
|
||||
const firstState: IFakeMcpClientState = { calls: [], closed: false };
|
||||
const secondState: IFakeMcpClientState = { calls: [], closed: false };
|
||||
|
||||
const mcpTools = await createMcpTools({
|
||||
servers: {
|
||||
'server one': {
|
||||
type: 'client',
|
||||
client: createFakeMcpClient('echo tool', 'first', firstState),
|
||||
},
|
||||
'server/two': {
|
||||
type: 'client',
|
||||
client: createFakeMcpClient('echo tool', 'second', secondState),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(mcpTools.tools).sort()).toEqual([
|
||||
'server_one__echo_tool',
|
||||
'server_two__echo_tool',
|
||||
]);
|
||||
expect(mcpTools.toolNameMap.server_one__echo_tool).toEqual({
|
||||
serverName: 'server one',
|
||||
toolName: 'echo tool',
|
||||
});
|
||||
|
||||
const output = await (mcpTools.tools.server_one__echo_tool as any).execute({ value: 'hello' });
|
||||
expect(output).toInclude('first: hello');
|
||||
expect(output).toInclude('Structured content');
|
||||
expect(firstState.calls).toEqual([
|
||||
{
|
||||
name: 'echo tool',
|
||||
arguments: { value: 'hello' },
|
||||
},
|
||||
]);
|
||||
|
||||
await mcpTools.close();
|
||||
expect(firstState.closed).toBeTrue();
|
||||
expect(secondState.closed).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('createMcpTools should support paginated tool discovery and custom formatting', async () => {
|
||||
const calls: Array<{ name: string; arguments?: Record<string, unknown> }> = [];
|
||||
const client: IMcpClientLike = {
|
||||
listTools: async (params) => ({
|
||||
tools: [
|
||||
{
|
||||
name: params?.cursor ? 'second tool' : 'first tool',
|
||||
inputSchema: { type: 'object' },
|
||||
},
|
||||
],
|
||||
nextCursor: params?.cursor ? undefined : 'next',
|
||||
} as any),
|
||||
callTool: async (params) => {
|
||||
calls.push(params);
|
||||
return { content: [{ type: 'text', text: 'ok' }] } as any;
|
||||
},
|
||||
};
|
||||
|
||||
const mcpTools = await createMcpTools({
|
||||
servers: {
|
||||
only: { type: 'client', client },
|
||||
},
|
||||
formatResult: (_result, context) => context.exposedToolName,
|
||||
});
|
||||
|
||||
expect(Object.keys(mcpTools.tools).sort()).toEqual(['first_tool', 'second_tool']);
|
||||
const output = await (mcpTools.tools.second_tool as any).execute('ignored');
|
||||
expect(output).toEqual('second_tool');
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
name: 'second tool',
|
||||
arguments: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
tap.test('formatMcpToolResult should handle MCP result shapes', async () => {
|
||||
expect(formatMcpToolResult({ toolResult: { ok: true } } as any)).toEqual({ ok: true });
|
||||
expect(formatMcpToolResult({ content: [{ type: 'text', text: 'failed' }], isError: true } as any)).toInclude(
|
||||
'MCP tool returned an error',
|
||||
);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartagent',
|
||||
version: '3.5.0',
|
||||
version: '3.7.0',
|
||||
description: 'Agentic loop for ai-sdk (Vercel AI SDK). Wraps streamText with stopWhen for parallel multi-step tool execution. Built on @push.rocks/smartai.'
|
||||
}
|
||||
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
import { Client, type ClientOptions } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import {
|
||||
StdioClientTransport,
|
||||
type StdioServerParameters,
|
||||
} from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import {
|
||||
StreamableHTTPClientTransport,
|
||||
type StreamableHTTPClientTransportOptions,
|
||||
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import { jsonSchema, tool } from '@push.rocks/smartai';
|
||||
import type { ToolSet } from '@push.rocks/smartai';
|
||||
|
||||
export type TMcpListToolsResult = Awaited<ReturnType<Client['listTools']>>;
|
||||
export type TMcpToolMetadata = TMcpListToolsResult['tools'][number];
|
||||
export type TMcpCallToolResult = Awaited<ReturnType<Client['callTool']>>;
|
||||
|
||||
export interface IMcpClientLike {
|
||||
listTools(params?: { cursor?: string }, options?: RequestOptions): Promise<TMcpListToolsResult>;
|
||||
callTool(
|
||||
params: { name: string; arguments?: Record<string, unknown> },
|
||||
resultSchema?: unknown,
|
||||
options?: RequestOptions,
|
||||
): Promise<TMcpCallToolResult>;
|
||||
close?(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IMcpServerClientOptions {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
clientOptions?: ClientOptions;
|
||||
requestOptions?: RequestOptions;
|
||||
}
|
||||
|
||||
export interface IMcpStdioServerConfig extends StdioServerParameters, IMcpServerClientOptions {
|
||||
type: 'stdio';
|
||||
}
|
||||
|
||||
export interface IMcpStreamableHttpServerConfig extends IMcpServerClientOptions {
|
||||
type: 'streamableHttp' | 'http';
|
||||
url: string | URL;
|
||||
transportOptions?: StreamableHTTPClientTransportOptions;
|
||||
}
|
||||
|
||||
export interface IMcpTransportServerConfig extends IMcpServerClientOptions {
|
||||
type: 'transport';
|
||||
transport: Transport;
|
||||
}
|
||||
|
||||
export interface IMcpClientServerConfig {
|
||||
type: 'client';
|
||||
client: IMcpClientLike;
|
||||
}
|
||||
|
||||
export type TMcpServerConfig =
|
||||
| IMcpStdioServerConfig
|
||||
| IMcpStreamableHttpServerConfig
|
||||
| IMcpTransportServerConfig
|
||||
| IMcpClientServerConfig;
|
||||
|
||||
export interface IMcpToolFormatContext {
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
exposedToolName: string;
|
||||
}
|
||||
|
||||
export interface ICreateMcpToolsOptions {
|
||||
servers: Record<string, TMcpServerConfig>;
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
clientOptions?: ClientOptions;
|
||||
requestOptions?: RequestOptions;
|
||||
listToolsRequestOptions?: RequestOptions;
|
||||
callToolRequestOptions?: RequestOptions;
|
||||
prefixToolNames?: boolean;
|
||||
formatResult?: (result: TMcpCallToolResult, context: IMcpToolFormatContext) => unknown;
|
||||
}
|
||||
|
||||
export interface IMcpConnectedServer {
|
||||
name: string;
|
||||
client: IMcpClientLike;
|
||||
tools: TMcpToolMetadata[];
|
||||
}
|
||||
|
||||
export interface IMcpToolNameMapping {
|
||||
serverName: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export interface ICreateMcpToolsResult {
|
||||
tools: ToolSet;
|
||||
servers: IMcpConnectedServer[];
|
||||
toolNameMap: Record<string, IMcpToolNameMapping>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const createMcpTools = async (
|
||||
options: ICreateMcpToolsOptions,
|
||||
): Promise<ICreateMcpToolsResult> => {
|
||||
const serverEntries = Object.entries(options.servers);
|
||||
if (serverEntries.length === 0) {
|
||||
throw new Error('createMcpTools requires at least one MCP server.');
|
||||
}
|
||||
|
||||
const connectedServers: IMcpConnectedServer[] = [];
|
||||
try {
|
||||
for (const [serverName, config] of serverEntries) {
|
||||
const client = await connectMcpServer(serverName, config, options);
|
||||
const tools = await listAllMcpTools(
|
||||
client,
|
||||
options.listToolsRequestOptions ?? getServerRequestOptions(config) ?? options.requestOptions,
|
||||
);
|
||||
connectedServers.push({ name: serverName, client, tools });
|
||||
}
|
||||
|
||||
const exposedTools: ToolSet = {};
|
||||
const toolNameMap: Record<string, IMcpToolNameMapping> = {};
|
||||
const usedToolNames = new Set<string>();
|
||||
const formatResult = options.formatResult ?? formatMcpToolResult;
|
||||
const shouldPrefixToolNames = options.prefixToolNames ?? connectedServers.length > 1;
|
||||
|
||||
for (const server of connectedServers) {
|
||||
for (const mcpTool of server.tools) {
|
||||
const baseToolName = createExposedToolName(server.name, mcpTool.name, shouldPrefixToolNames);
|
||||
const exposedToolName = createUniqueToolName(baseToolName, usedToolNames);
|
||||
usedToolNames.add(exposedToolName);
|
||||
toolNameMap[exposedToolName] = {
|
||||
serverName: server.name,
|
||||
toolName: mcpTool.name,
|
||||
};
|
||||
|
||||
exposedTools[exposedToolName] = tool({
|
||||
description: mcpTool.description ?? `MCP tool ${mcpTool.name} from ${server.name}`,
|
||||
inputSchema: jsonSchema(mcpTool.inputSchema as never),
|
||||
execute: async (input: unknown) => {
|
||||
const result = await server.client.callTool(
|
||||
{
|
||||
name: mcpTool.name,
|
||||
arguments: normalizeMcpArguments(input),
|
||||
},
|
||||
undefined,
|
||||
options.callToolRequestOptions ?? options.requestOptions,
|
||||
);
|
||||
return formatResult(result, {
|
||||
serverName: server.name,
|
||||
toolName: mcpTool.name,
|
||||
exposedToolName,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tools: exposedTools,
|
||||
servers: connectedServers,
|
||||
toolNameMap,
|
||||
close: async () => closeMcpClients(connectedServers),
|
||||
};
|
||||
} catch (error) {
|
||||
await closeMcpClients(connectedServers);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const sanitizeMcpToolName = (name: string, fallback = 'mcp_tool'): string => {
|
||||
const sanitized = name
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
const safeName = sanitized || fallback;
|
||||
return /^[a-zA-Z_]/.test(safeName) ? safeName : `_${safeName}`;
|
||||
};
|
||||
|
||||
export const formatMcpToolResult = (result: TMcpCallToolResult): unknown => {
|
||||
const resultRecord = result as Record<string, unknown>;
|
||||
if (Object.prototype.hasOwnProperty.call(resultRecord, 'toolResult')) {
|
||||
return resultRecord.toolResult;
|
||||
}
|
||||
|
||||
const content = Array.isArray(resultRecord.content)
|
||||
? resultRecord.content.map(formatMcpContentPart).filter(Boolean).join('\n\n')
|
||||
: '';
|
||||
const formattedContent = resultRecord.isError && content
|
||||
? `MCP tool returned an error:\n${content}`
|
||||
: content;
|
||||
const structuredContent = resultRecord.structuredContent;
|
||||
|
||||
if (structuredContent !== undefined && formattedContent) {
|
||||
return `${formattedContent}\n\nStructured content:\n${stringifyJson(structuredContent)}`;
|
||||
}
|
||||
if (structuredContent !== undefined) {
|
||||
return structuredContent;
|
||||
}
|
||||
if (formattedContent) {
|
||||
return formattedContent;
|
||||
}
|
||||
return stringifyJson(result);
|
||||
};
|
||||
|
||||
const connectMcpServer = async (
|
||||
serverName: string,
|
||||
config: TMcpServerConfig,
|
||||
options: ICreateMcpToolsOptions,
|
||||
): Promise<IMcpClientLike> => {
|
||||
if (config.type === 'client') {
|
||||
return config.client;
|
||||
}
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: config.clientName ?? options.clientName ?? 'smartagent-mcp',
|
||||
version: config.clientVersion ?? options.clientVersion ?? '1.0.0',
|
||||
},
|
||||
config.clientOptions ?? options.clientOptions ?? { capabilities: {} },
|
||||
);
|
||||
const transport = createMcpTransport(config);
|
||||
await client.connect(transport, config.requestOptions ?? options.requestOptions);
|
||||
return client;
|
||||
};
|
||||
|
||||
const createMcpTransport = (
|
||||
config: Exclude<TMcpServerConfig, IMcpClientServerConfig>,
|
||||
): Transport => {
|
||||
if (config.type === 'stdio') {
|
||||
const { type, clientName, clientVersion, clientOptions, requestOptions, ...serverParameters } = config;
|
||||
return new StdioClientTransport(serverParameters);
|
||||
}
|
||||
|
||||
if (config.type === 'streamableHttp' || config.type === 'http') {
|
||||
return new StreamableHTTPClientTransport(new URL(config.url), config.transportOptions);
|
||||
}
|
||||
|
||||
if (config.type === 'transport') {
|
||||
return config.transport;
|
||||
}
|
||||
|
||||
throw new Error('Unsupported MCP server transport.');
|
||||
};
|
||||
|
||||
const getServerRequestOptions = (config: TMcpServerConfig): RequestOptions | undefined => {
|
||||
return config.type === 'client' ? undefined : config.requestOptions;
|
||||
};
|
||||
|
||||
const listAllMcpTools = async (
|
||||
client: IMcpClientLike,
|
||||
requestOptions?: RequestOptions,
|
||||
): Promise<TMcpToolMetadata[]> => {
|
||||
const tools: TMcpToolMetadata[] = [];
|
||||
let cursor: string | undefined;
|
||||
do {
|
||||
const result = await client.listTools(cursor ? { cursor } : undefined, requestOptions);
|
||||
tools.push(...result.tools);
|
||||
cursor = result.nextCursor;
|
||||
} while (cursor);
|
||||
return tools;
|
||||
};
|
||||
|
||||
const createExposedToolName = (
|
||||
serverName: string,
|
||||
toolName: string,
|
||||
prefixToolName: boolean,
|
||||
): string => {
|
||||
const safeToolName = sanitizeMcpToolName(toolName);
|
||||
if (!prefixToolName) {
|
||||
return safeToolName;
|
||||
}
|
||||
return `${sanitizeMcpToolName(serverName, 'mcp')}__${safeToolName}`;
|
||||
};
|
||||
|
||||
const createUniqueToolName = (baseToolName: string, usedToolNames: Set<string>): string => {
|
||||
if (!usedToolNames.has(baseToolName)) {
|
||||
return baseToolName;
|
||||
}
|
||||
let suffix = 2;
|
||||
while (usedToolNames.has(`${baseToolName}_${suffix}`)) {
|
||||
suffix++;
|
||||
}
|
||||
return `${baseToolName}_${suffix}`;
|
||||
};
|
||||
|
||||
const normalizeMcpArguments = (input: unknown): Record<string, unknown> => {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
return {};
|
||||
}
|
||||
return input as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const formatMcpContentPart = (part: unknown): string => {
|
||||
if (!part || typeof part !== 'object') {
|
||||
return stringifyJson(part);
|
||||
}
|
||||
const partRecord = part as Record<string, unknown>;
|
||||
const type = partRecord.type;
|
||||
if (type === 'text') {
|
||||
return typeof partRecord.text === 'string' ? partRecord.text : '';
|
||||
}
|
||||
if (type === 'image' || type === 'audio') {
|
||||
const data = typeof partRecord.data === 'string' ? partRecord.data : '';
|
||||
const mimeType = typeof partRecord.mimeType === 'string' ? partRecord.mimeType : 'application/octet-stream';
|
||||
return `[${type}: ${mimeType}; base64 length ${data.length}]`;
|
||||
}
|
||||
if (type === 'resource') {
|
||||
return formatMcpResourcePart(partRecord.resource);
|
||||
}
|
||||
if (type === 'resource_link') {
|
||||
const name = typeof partRecord.name === 'string' ? partRecord.name : 'resource';
|
||||
const uri = typeof partRecord.uri === 'string' ? partRecord.uri : '';
|
||||
return `Resource link ${name}${uri ? `: ${uri}` : ''}`;
|
||||
}
|
||||
return stringifyJson(part);
|
||||
};
|
||||
|
||||
const formatMcpResourcePart = (resource: unknown): string => {
|
||||
if (!resource || typeof resource !== 'object') {
|
||||
return stringifyJson(resource);
|
||||
}
|
||||
const resourceRecord = resource as Record<string, unknown>;
|
||||
const uri = typeof resourceRecord.uri === 'string' ? resourceRecord.uri : 'resource';
|
||||
if (typeof resourceRecord.text === 'string') {
|
||||
return `Resource ${uri}:\n${resourceRecord.text}`;
|
||||
}
|
||||
if (typeof resourceRecord.blob === 'string') {
|
||||
const mimeType = typeof resourceRecord.mimeType === 'string' ? resourceRecord.mimeType : 'application/octet-stream';
|
||||
return `[resource ${uri}: ${mimeType}; base64 length ${resourceRecord.blob.length}]`;
|
||||
}
|
||||
return stringifyJson(resource);
|
||||
};
|
||||
|
||||
const stringifyJson = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value, undefined, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const closeMcpClients = async (servers: IMcpConnectedServer[]): Promise<void> => {
|
||||
await Promise.all(servers.map((server) => server.client.close?.()));
|
||||
};
|
||||
Reference in New Issue
Block a user