源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 { createHash } from 'node:crypto';
|
||||
import { jsonSchema } from 'ai';
|
||||
import type { ToolActivityKind } from '@maka/core/events';
|
||||
import type {
|
||||
McpCallResult,
|
||||
McpToolBinding,
|
||||
McpToolDescriptor,
|
||||
McpToolSnapshot,
|
||||
} from '@maka/core/mcp';
|
||||
import type { PermissionMode, ToolCategory } from '@maka/core/permission';
|
||||
import type { ExecutionBoundary } from '@maka/core/sandbox-boundary';
|
||||
import type { ToolRecoveryMode } from '@maka/core/runtime-event';
|
||||
import type { ToolResultContentPart, ToolResultOutput } from './model-protocol.js';
|
||||
import type { MakaTool } from './tool-runtime.js';
|
||||
|
||||
const MAX_PROVIDER_TOOL_NAME = 64;
|
||||
const HASH_CHARS = 10;
|
||||
const MAX_NATIVE_IMAGE_BASE64_CHARS = 20_000_000;
|
||||
const MAX_NATIVE_IMAGES = 4;
|
||||
const MAX_MODEL_TEXT_CHARS = 200_000;
|
||||
const MAX_SUMMARIZED_BLOCKS = 100;
|
||||
const TRUNCATION_MARKER = '\n…[truncated by Maka]';
|
||||
|
||||
export interface McpToolProvider {
|
||||
toolSnapshot(): McpToolSnapshot;
|
||||
prepareTool?(
|
||||
binding: McpToolBinding,
|
||||
args: Record<string, unknown>,
|
||||
options: Omit<McpToolCallOptions, 'emitProgress'>,
|
||||
): Promise<McpPreparedToolCall>;
|
||||
callTool(
|
||||
binding: McpToolBinding,
|
||||
args: Record<string, unknown>,
|
||||
options: McpToolCallOptions,
|
||||
): Promise<McpCallResult>;
|
||||
}
|
||||
|
||||
export interface McpPreparedToolCall {
|
||||
execute(options?: {
|
||||
readonly emitProgress?: (current: number, total: number) => void;
|
||||
}): Promise<McpCallResult>;
|
||||
cancel(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface McpToolCallOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
readonly timeoutMs?: number;
|
||||
readonly context: McpToolInvocationContext;
|
||||
readonly emitProgress?: (current: number, total: number) => void;
|
||||
}
|
||||
|
||||
export interface McpToolInvocationContext {
|
||||
readonly sessionId: string;
|
||||
readonly runId?: string;
|
||||
readonly turnId: string;
|
||||
readonly toolCallId: string;
|
||||
readonly cwd: string;
|
||||
readonly executionBoundary?: ExecutionBoundary;
|
||||
readonly permissionMode?: PermissionMode;
|
||||
}
|
||||
|
||||
export interface BuildMcpToolsOptions {
|
||||
callTimeoutMs?: number;
|
||||
categoryHint?: ToolCategory;
|
||||
hostAdmission?: MakaTool['hostAdmission'];
|
||||
recoveryMode?: ToolRecoveryMode;
|
||||
executionLocation?: 'host' | 'remote';
|
||||
activityKindForDescriptor?: (descriptor: McpToolDescriptor) => ToolActivityKind | undefined;
|
||||
}
|
||||
|
||||
export function buildMcpTools(
|
||||
provider: McpToolProvider,
|
||||
options: BuildMcpToolsOptions = {},
|
||||
): MakaTool[] {
|
||||
const names = new Map<string, string>();
|
||||
const snapshot = provider.toolSnapshot();
|
||||
return snapshot.tools.map(({ descriptor, binding }) => {
|
||||
const identity = `${descriptor.serverId}\0${descriptor.name}`;
|
||||
const name = mcpProxyToolName(descriptor.serverId, descriptor.name);
|
||||
const collision = names.get(name);
|
||||
if (collision && collision !== identity) {
|
||||
throw new Error(`MCP proxy tool name collision: ${name}`);
|
||||
}
|
||||
names.set(name, identity);
|
||||
return {
|
||||
name,
|
||||
description:
|
||||
descriptor.description?.trim() ||
|
||||
`MCP tool ${descriptor.name} provided by ${descriptor.serverId}`,
|
||||
displayName: descriptor.annotations?.title?.trim() || descriptor.name,
|
||||
activityKind: options.activityKindForDescriptor?.(descriptor) ?? 'tool',
|
||||
// MCP annotations are advisory provider claims, not a security boundary.
|
||||
// The trusted composition may select a stricter open-world category;
|
||||
// ordinary MCP servers retain the side-effecting network default.
|
||||
categoryHint: options.categoryHint ?? 'network_send',
|
||||
...(options.hostAdmission ? { hostAdmission: options.hostAdmission } : {}),
|
||||
...(options.recoveryMode ? { recoveryMode: options.recoveryMode } : {}),
|
||||
parameters: jsonSchema(descriptor.inputSchema),
|
||||
...(provider.prepareTool
|
||||
? {
|
||||
prepareExecution: async (args: unknown, context) => {
|
||||
const prepared = await provider.prepareTool!(binding, asArguments(args), {
|
||||
signal: context.abortSignal,
|
||||
timeoutMs: options.callTimeoutMs,
|
||||
context: {
|
||||
sessionId: context.sessionId,
|
||||
runId: context.runId,
|
||||
turnId: context.turnId,
|
||||
toolCallId: context.toolCallId,
|
||||
cwd: context.cwd,
|
||||
executionBoundary: context.executionBoundary,
|
||||
permissionMode: context.permissionMode,
|
||||
},
|
||||
});
|
||||
return {
|
||||
execute: (executionContext) =>
|
||||
prepared.execute({
|
||||
...(executionContext.emitProgress
|
||||
? { emitProgress: executionContext.emitProgress }
|
||||
: {}),
|
||||
}),
|
||||
cancel: () => prepared.cancel(),
|
||||
};
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
impl: async (args: unknown, context) => {
|
||||
// Managed network authority applies equally to Direct and nested CodeMode dispatch.
|
||||
if (
|
||||
options.executionLocation !== 'remote' &&
|
||||
context.executionBoundary?.kind === 'managed' &&
|
||||
context.executionBoundary.profile.network.kind !== 'enabled'
|
||||
) {
|
||||
if (!context.requestSandboxBoundary) {
|
||||
throw new Error('MCP network access requires sandbox boundary approval');
|
||||
}
|
||||
const settlement = await context.requestSandboxBoundary(
|
||||
{ network: { enabled: true } },
|
||||
`Call MCP tool ${descriptor.serverId}/${descriptor.name}.`,
|
||||
);
|
||||
if (settlement.request.status !== 'approved') {
|
||||
throw new Error('MCP network access denied');
|
||||
}
|
||||
}
|
||||
return provider.callTool(binding, asArguments(args), {
|
||||
signal: context.abortSignal,
|
||||
timeoutMs: options.callTimeoutMs,
|
||||
context: {
|
||||
sessionId: context.sessionId,
|
||||
turnId: context.turnId,
|
||||
toolCallId: context.toolCallId,
|
||||
cwd: context.cwd,
|
||||
},
|
||||
...(context.emitProgress ? { emitProgress: context.emitProgress } : {}),
|
||||
});
|
||||
},
|
||||
toModelOutput: ({ output }) => mcpResultToModelOutput(output),
|
||||
} satisfies MakaTool;
|
||||
});
|
||||
}
|
||||
|
||||
export function mcpProxyToolName(serverId: string, toolName: string): string {
|
||||
const raw = `mcp__${sanitizeNamePart(serverId)}__${sanitizeNamePart(toolName)}`;
|
||||
if (raw.length <= MAX_PROVIDER_TOOL_NAME) return raw;
|
||||
const hash = createHash('sha256')
|
||||
.update(`${serverId}\0${toolName}`)
|
||||
.digest('hex')
|
||||
.slice(0, HASH_CHARS);
|
||||
return `${raw.slice(0, MAX_PROVIDER_TOOL_NAME - HASH_CHARS - 2)}__${hash}`;
|
||||
}
|
||||
|
||||
function sanitizeNamePart(value: string): string {
|
||||
const sanitized = value
|
||||
.normalize('NFKD')
|
||||
.replace(/[^A-Za-z0-9_-]+/gu, '_')
|
||||
.replace(/^_+|_+$/gu, '');
|
||||
return sanitized || 'unnamed';
|
||||
}
|
||||
|
||||
function asArguments(value: unknown): Record<string, unknown> {
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
throw new Error('MCP tool arguments must be an object');
|
||||
}
|
||||
|
||||
function mcpResultToModelOutput(output: unknown): Extract<ToolResultOutput, { type: 'content' }> {
|
||||
const result = output as Partial<McpCallResult>;
|
||||
const blocks = Array.isArray(result.content) ? result.content : [];
|
||||
const value: ToolResultContentPart[] = [];
|
||||
const nonVisual: unknown[] = [];
|
||||
let remainingTextChars = MAX_MODEL_TEXT_CHARS;
|
||||
let imageChars = 0;
|
||||
let imageCount = 0;
|
||||
let omittedSummaryBlocks = 0;
|
||||
|
||||
const appendText = (text: string): void => {
|
||||
if (remainingTextChars <= 0) return;
|
||||
const clipped = clipModelText(text, remainingTextChars);
|
||||
remainingTextChars -= clipped.length;
|
||||
value.push({ type: 'text', text: clipped });
|
||||
};
|
||||
const appendSummary = (summary: unknown): void => {
|
||||
if (nonVisual.length < MAX_SUMMARIZED_BLOCKS) nonVisual.push(summary);
|
||||
else omittedSummaryBlocks += 1;
|
||||
};
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'text') appendText(block.text);
|
||||
else if (
|
||||
block.type === 'image' &&
|
||||
imageCount < MAX_NATIVE_IMAGES &&
|
||||
imageChars + block.data.length <= MAX_NATIVE_IMAGE_BASE64_CHARS
|
||||
) {
|
||||
value.push({
|
||||
type: 'file',
|
||||
data: { type: 'data', data: block.data },
|
||||
mediaType: block.mimeType,
|
||||
});
|
||||
imageCount += 1;
|
||||
imageChars += block.data.length;
|
||||
} else appendSummary(summarizeNonVisualBlock(block));
|
||||
}
|
||||
if (nonVisual.length || omittedSummaryBlocks || result.structuredContent !== undefined) {
|
||||
appendText(
|
||||
safeJsonStringify({
|
||||
...(nonVisual.length ? { content: nonVisual } : {}),
|
||||
...(omittedSummaryBlocks ? { omittedContentBlocks: omittedSummaryBlocks } : {}),
|
||||
...(result.structuredContent !== undefined
|
||||
? { structuredContent: result.structuredContent }
|
||||
: {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (value.length === 0) value.push({ type: 'text', text: 'MCP tool completed with no content.' });
|
||||
return { type: 'content', value };
|
||||
}
|
||||
|
||||
function summarizeNonVisualBlock(block: McpCallResult['content'][number]): unknown {
|
||||
if (block.type === 'audio') {
|
||||
return {
|
||||
type: block.type,
|
||||
mimeType: block.mimeType,
|
||||
base64Chars: block.data.length,
|
||||
};
|
||||
}
|
||||
if (block.type === 'resource') {
|
||||
return {
|
||||
...block,
|
||||
...(block.text ? { text: clipModelText(block.text, MAX_MODEL_TEXT_CHARS) } : {}),
|
||||
...(block.blob ? { blob: undefined, base64Chars: block.blob.length } : {}),
|
||||
};
|
||||
}
|
||||
if (block.type === 'image') {
|
||||
return {
|
||||
type: block.type,
|
||||
mimeType: block.mimeType,
|
||||
base64Chars: block.data.length,
|
||||
omitted: 'too_large',
|
||||
};
|
||||
}
|
||||
if (block.type === 'unknown') return { type: block.type, omitted: true };
|
||||
return block;
|
||||
}
|
||||
|
||||
function clipModelText(value: string, limit: number): string {
|
||||
if (value.length <= limit) return value;
|
||||
if (limit <= TRUNCATION_MARKER.length) return TRUNCATION_MARKER.slice(0, limit);
|
||||
return `${value.slice(0, limit - TRUNCATION_MARKER.length)}${TRUNCATION_MARKER}`;
|
||||
}
|
||||
|
||||
function safeJsonStringify(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return '{"content":"MCP output could not be serialized"}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user