autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
514 lines (513 loc) • 23.2 kB
JavaScript
/**
* AutoSnippet V3 MCP Server — 整合版
*
* Model Context Protocol (stdio transport)
* 提供给 IDE AI Agent (Cursor/VSCode Copilot) 的工具集
*
* V3.3 整合:39 → 16 工具(14 agent + 2 admin)
* 通过 ASD_MCP_TIER 环境变量控制可见工具集(agent/admin)
*
* 冷启动双路径:
* - 外部 Agent 路径: bootstrap (Mission Briefing) → dimension_complete × N → wiki(plan) → wiki(finalize)
* - 内部 Agent 路径: bootstrap.js bootstrapKnowledge() → orchestrator.js AI pipeline (Phase 5)
*
* Gateway 权限 gating: 写操作经过 Gateway 权限/宪法/审计检查(支持动态 resolver)
*
* 本文件仅包含服务编排层(初始化、路由、Gateway gating、生命周期)。
* 工具定义 → tools.js
* Handler 实现 → handlers/*.js
* 整合路由 → handlers/consolidated.js
*/
import { McpServer as SdkMcpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { CapabilityProbe } from '#core/capability/CapabilityProbe.js';
import Logger from '#infra/logging/Logger.js';
import { applyPendingAutoApprove, markAutoApproveNeeded } from './autoApproveInjector.js';
import { envelope } from './envelope.js';
import { wrapHandler } from './errorHandler.js';
import { createIdleIntent } from './handlers/types.js';
import { TIER_ORDER, TOOL_GATEWAY_MAP, TOOLS } from './tools.js';
// ─── Handler 模块 ─────────────────────────────────────────────
import * as candidateHandlers from './handlers/candidate.js';
import * as consolidated from './handlers/consolidated.js';
import * as knowledgeHandlers from './handlers/knowledge.js';
import * as systemHandlers from './handlers/system.js';
// ─── External Agent Bootstrap 新 handler ──────────────────────
import { bootstrapExternal } from './handlers/bootstrap-external.js';
import { dimensionComplete } from './handlers/dimension-complete-external.js';
import { evolveExternal } from './handlers/evolve-external.js';
import { panoramaHandler } from './handlers/panorama.js';
import { rescanExternal } from './handlers/rescan-external.js';
import { taskHandler } from './handlers/task.js';
import { wikiRouter } from './handlers/wiki-external.js';
// ─── McpServer 类 ─────────────────────────────────────────────
export class McpServer {
container;
logger;
_autoApproveMarked;
_capabilityProbe;
_lastTaskOperation;
_session;
_startedAt;
bootstrap;
sdkServer;
constructor(options = {}) {
this.logger = Logger.getInstance();
this.container = options.container || null;
this.bootstrap = options.bootstrap || null;
this.sdkServer = null;
this._startedAt = Date.now();
this._autoApproveMarked = false;
this._capabilityProbe = null;
this._lastTaskOperation = '';
// ── Session 管理 (with intent lifecycle) ──
this._session = {
id: `ses-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
startedAt: Date.now(),
toolCallCount: 0,
toolsUsed: new Set(),
lastActivityAt: Date.now(),
intent: createIdleIntent(),
};
}
/** 共享上下文对象,传给所有 handler */
get _ctx() {
return {
container: this.container,
logger: this.logger,
startedAt: this._startedAt,
session: this._session,
};
}
async initialize() {
if (!this.container) {
const { default: Bootstrap } = await import('../../bootstrap.js');
// MCP 模式必须显式指定项目目录 — process.cwd() 在多根工作区中不可靠
const projectRoot = process.env.ASD_PROJECT_DIR;
if (!projectRoot) {
const msg = `[MCP] 缺少 ASD_PROJECT_DIR 环境变量。MCP server 拒绝启动。\n` +
`在多根工作区中 process.cwd() 可能指向任意子目录,不能作为项目根目录。\n` +
`请在 .vscode/mcp.json 的 env 中设置 ASD_PROJECT_DIR 为目标项目的绝对路径。`;
process.stderr.write(`${msg}\n`);
throw new Error(msg);
}
// ── 排除项目检查 — 防止误配置 ASD_PROJECT_DIR 到不该创建运行时数据的目录 ──
const { isExcludedProject } = await import('../../shared/isOwnDevRepo.js');
const exclusion = isExcludedProject(projectRoot);
if (exclusion.excluded) {
const msg = `[MCP] projectRoot "${projectRoot}" 是排除项目(${exclusion.reason}),` +
`MCP server 拒绝在此目录创建运行时数据。\n` +
`提示: 在 .vscode/mcp.json 的 env 中设置正确的 ASD_PROJECT_DIR。`;
process.stderr.write(`${msg}\n`);
throw new Error(msg);
}
// 切换工作目录到项目根 — 确保 DB 等相对路径正确解析
if (projectRoot !== process.cwd()) {
process.chdir(projectRoot);
}
Bootstrap.configurePathGuard(projectRoot);
this.bootstrap = new Bootstrap();
const components = await this.bootstrap.initialize();
// 将 Bootstrap 组件注入 ServiceContainer
const { getServiceContainer } = await import('#inject/ServiceContainer.js');
this.container = getServiceContainer();
await this.container.initialize({
db: components.db,
auditLogger: components.auditLogger,
gateway: components.gateway,
constitution: components.constitution,
config: components.config,
skillHooks: components.skillHooks,
projectRoot,
});
// 注册 Gateway action handlers
const { registerGatewayActions } = await import('#core/gateway/GatewayActionRegistry.js');
const gateway = this.container.get('gateway');
if (gateway) {
registerGatewayActions(gateway, this.container);
}
}
this.sdkServer = new SdkMcpServer({ name: 'autosnippet-v3', version: '3.0.0' }, { capabilities: { tools: {} } });
this._registerHandlers();
return this;
}
/**
* 注册 ListTools / CallTool 请求处理器
* ListTools 基于 ASD_MCP_TIER 过滤可见工具
*/
_registerHandlers() {
// ── ListTools: 按 tier 过滤 ──
this.sdkServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tierName = process.env.ASD_MCP_TIER || 'agent';
const maxTier = TIER_ORDER[tierName] ?? TIER_ORDER.agent;
const visible = TOOLS.filter((t) => (TIER_ORDER[t.tier || 'agent'] ?? 0) <= maxTier);
return { tools: visible };
});
// ── CallTool: 路由到 handler ──
this.sdkServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const t0 = Date.now();
try {
const result = await this._handleToolCall(name, args || {});
return {
content: [
{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
},
],
};
}
catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
this.logger.error(`MCP tool error: ${name}`, { error: errMsg });
const env = envelope({
success: false,
message: errMsg,
errorCode: 'TOOL_ERROR',
meta: { tool: name, responseTimeMs: Date.now() - t0 },
});
return { content: [{ type: 'text', text: JSON.stringify(env, null, 2) }], isError: true };
}
});
}
async _handleToolCall(name, args) {
// ── Gateway 权限 gating(写操作) ──
await this._gatewayGate(name, args);
const ctx = this._ctx;
// 查找 handler 并通过 wrapHandler 统一错误处理
const handler = this._resolveHandler(name);
if (!handler) {
throw new Error(`Unknown tool: ${name}`);
}
const wrapped = wrapHandler(name, handler);
// Track task operation for _injectDecisions
if (name === 'autosnippet_task') {
this._lastTaskOperation = args.operation || '';
}
const result = await wrapped(ctx, args);
// ── Session 追踪 + 行为采集 ──
this._trackSession(name, result);
// ── [DEFERRED] Decision 注入(待 JSONL 数据验证后启用) ──
// await this._injectDecisions(name, result);
// ── 首次成功 tool call → 标记 autoApprove(one-shot) ──
// 用户已手动授权了至少一个工具,标记后下次 MCP 启动注入 autoApprove
if (!this._autoApproveMarked) {
this._autoApproveMarked = true;
try {
const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
markAutoApproveNeeded(projectRoot, this.logger);
}
catch {
/* non-blocking */
}
}
return result;
}
// ─── Session tracking + behavior collection ─────────────
/**
* Post-tool-call hook: update session stats + intent behavior tracking.
* Always called (non-blocking, synchronous).
*
* - Session stats: toolCallCount, toolsUsed, lastActivityAt
* - Intent tracking (when active): toolCalls, searchQueries, mentionedFiles, drift detection
*/
_trackSession(toolName, result) {
// ── Session stats (always) ──
this._session.toolCallCount++;
this._session.toolsUsed.add(toolName);
this._session.lastActivityAt = Date.now();
// Task handler manages IntentState internally — skip behavior tracking
if (toolName === 'autosnippet_task') {
return;
}
// ── Intent behavior tracking (active intent only) ──
const intent = this._session.intent;
if (intent.phase !== 'active') {
return;
}
// Track tool call
intent.toolCalls.push({
tool: toolName,
timestamp: Date.now(),
args_summary: toolName,
});
// Auto-collect search queries
if (toolName === 'autosnippet_search') {
const query = this._extractSearchQuery(result);
if (query) {
intent.searchQueries.push(query);
}
}
// Auto-collect mentioned files
const files = this._extractMentionedFiles(toolName, result);
for (const f of files) {
if (!intent.mentionedFiles.includes(f)) {
intent.mentionedFiles.push(f);
const mod = this._inferModule(f);
if (mod) {
intent.mentionedModules.add(mod);
}
}
}
// Drift detection
this._detectDrift(toolName, intent);
}
// ─── [DEFERRED] Decision injection ───────────────────────
/**
* Inject active decisions + intent context into tool results.
* Currently deferred — enable by uncommenting the call in _handleToolCall.
*/
async _injectDecisions(toolName, result) {
if (toolName === 'autosnippet_task') {
return result;
}
const intent = this._session.intent;
if (intent.phase !== 'active') {
return result;
}
if (intent.decisions.length > 0 && typeof result === 'object' && result !== null) {
const resultObj = result;
resultObj._activeDecisions = intent.decisions.map((d) => ({
id: d.id,
title: d.title,
}));
resultObj._intentContext =
`Active intent: "${intent.primeQuery || '(no query)'}"` +
(intent.taskId ? ` | Task: ${intent.taskId}` : '') +
` | ${intent.toolCalls.length} tool calls | ${intent.decisions.length} decision(s)`;
}
return result;
}
// ─── Drift detection helpers ───────────────────
_detectDrift(toolName, intent) {
for (const mod of intent.mentionedModules) {
if (intent.primeModule && mod !== intent.primeModule) {
const alreadyDrifted = intent.driftEvents.some((d) => d.type === 'new_module' && d.detail.includes(mod));
if (!alreadyDrifted) {
intent.driftEvents.push({
timestamp: Date.now(),
trigger: toolName,
type: 'new_module',
detail: `New module: ${mod} (prime: ${intent.primeModule})`,
primeOverlap: this._computeOverlap(mod, intent.primeQuery),
});
}
}
}
if (toolName === 'autosnippet_search' && intent.searchQueries.length > 0) {
const latestQuery = intent.searchQueries[intent.searchQueries.length - 1];
const overlap = this._computeKeywordOverlap(latestQuery, intent.primeQuery);
if (overlap < 0.3) {
intent.driftEvents.push({
timestamp: Date.now(),
trigger: toolName,
type: 'search_shift',
detail: `Search drift: "${latestQuery.slice(0, 40)}" (overlap: ${Math.round(overlap * 100)}%)`,
primeOverlap: overlap,
});
}
}
}
_computeKeywordOverlap(a, b) {
if (!a || !b) {
return 0;
}
const tokensA = new Set(a
.toLowerCase()
.split(/[\s,./\\|]+/)
.filter((t) => t.length > 1));
const tokensB = new Set(b
.toLowerCase()
.split(/[\s,./\\|]+/)
.filter((t) => t.length > 1));
if (tokensA.size === 0 || tokensB.size === 0) {
return 0;
}
let shared = 0;
for (const t of tokensA) {
if (tokensB.has(t)) {
shared++;
}
}
return shared / Math.max(tokensA.size, tokensB.size);
}
_computeOverlap(term, query) {
if (!term || !query) {
return 0;
}
return query.toLowerCase().includes(term.toLowerCase()) ? 1 : 0;
}
_extractSearchQuery(result) {
if (typeof result === 'object' && result !== null) {
const obj = result;
if (typeof obj.query === 'string') {
return obj.query;
}
}
return null;
}
_extractMentionedFiles(_toolName, result) {
if (typeof result === 'object' && result !== null) {
const obj = result;
const files = obj.files || obj.mentionedFiles;
if (Array.isArray(files)) {
return files.filter((f) => typeof f === 'string');
}
}
return [];
}
_inferModule(filePath) {
const parts = filePath.replace(/\\/g, '/').split('/');
const meaningful = parts.slice(1, -1).filter((p) => !['src', 'lib', 'Sources'].includes(p));
return meaningful.slice(0, 2).join('/') || null;
}
/**
* 解析工具名到 handler 函数(V3 整合版)
*/
_resolveHandler(name) {
const HANDLER_MAP = {
// ── Agent 层 ──
autosnippet_health: (ctx) => systemHandlers.health(ctx),
autosnippet_search: (ctx, args) => consolidated.consolidatedSearch(ctx, args),
autosnippet_knowledge: (ctx, args) => consolidated.consolidatedKnowledge(ctx, args),
autosnippet_structure: (ctx, args) => consolidated.consolidatedStructure(ctx, args),
autosnippet_call_context: (ctx, args) => consolidated.consolidatedCallContext(ctx, args),
autosnippet_graph: (ctx, args) => consolidated.consolidatedGraph(ctx, args),
autosnippet_guard: (ctx, args) => consolidated.consolidatedGuard(ctx, args),
autosnippet_submit_knowledge: (ctx, args) => consolidated.enhancedSubmitKnowledge(ctx, args),
autosnippet_skill: (ctx, args) => consolidated.consolidatedSkill(ctx, args),
autosnippet_task: (ctx, args) => taskHandler(ctx, args),
autosnippet_panorama: (ctx, args) => panoramaHandler(ctx, args),
// ── External Agent Bootstrap (v3.1) ──
autosnippet_bootstrap: (ctx, _args) => bootstrapExternal(ctx),
autosnippet_rescan: (ctx, args) => rescanExternal(ctx, args),
autosnippet_evolve: (ctx, args) => evolveExternal(ctx, args),
autosnippet_dimension_complete: (ctx, args) => dimensionComplete(ctx, args),
autosnippet_wiki: (ctx, args) => wikiRouter(ctx, args),
// ── Admin 层 (+4) ──
autosnippet_enrich_candidates: (ctx, args) => candidateHandlers.enrichCandidates(ctx, args),
autosnippet_knowledge_lifecycle: (ctx, args) => knowledgeHandlers.knowledgeLifecycle(ctx, args),
};
return HANDLER_MAP[name] ?? null;
}
/**
* 获取(或懒创建)CapabilityProbe 实例,用于探测子仓库写权限
* 配置来自 constitution capabilities.git_write
*/
_getCapabilityProbe() {
if (!this._capabilityProbe) {
try {
const constitution = this.container?.get('constitution');
const caps = constitution?.config?.capabilities?.git_write || {};
this._capabilityProbe = new CapabilityProbe({
cacheTTL: caps.cache_ttl || 86400,
noRemote: caps.no_remote || 'allow',
});
}
catch {
this._capabilityProbe = new CapabilityProbe();
}
}
return this._capabilityProbe;
}
/**
* Gateway 权限 gating — 写操作验证权限/宪法/审计
* 只读工具直接跳过(不在 TOOL_GATEWAY_MAP 中)
* 支持动态 resolver(operation-based 工具按参数解析 action/resource)
*
* actor 解析:使用 CapabilityProbe 探测本地用户的子仓库权限
* - admin → 'developer' 全权限
* - contributor → 'contributor' 只读,写操作被拒绝
* - visitor → 'visitor' 最小权限
* 探测失败时降级为 'external_agent'(向后兼容)
*/
async _gatewayGate(toolName, args) {
let mapping = TOOL_GATEWAY_MAP[toolName];
if (!mapping) {
return; // 只读工具,跳过
}
// 动态 resolver:根据 args 计算实际 action/resource
if (typeof mapping.resolver === 'function') {
mapping = mapping.resolver(args);
if (!mapping) {
return; // resolver 返回 null 表示只读操作
}
}
try {
const gateway = this.container?.get('gateway');
if (!gateway) {
return; // Gateway 未初始化,降级放行
}
// 用 CapabilityProbe 确定本地用户角色
let actor = 'external_agent';
try {
const probe = this._getCapabilityProbe();
actor = probe.probeRole();
}
catch {
// 探测失败降级为 external_agent
}
const result = await gateway.checkOnly({
actor,
action: mapping.action,
resource: mapping.resource,
data: args || {},
});
if (!result.success) {
const code = result.error?.code || 'PERMISSION_DENIED';
const msg = result.error?.message || 'Gateway permission check failed';
this.logger.warn(`MCP Gateway gating denied: ${toolName}`, { code, msg, actor });
throw new Error(`[${code}] ${msg}`);
}
this.logger.debug(`MCP Gateway gating passed: ${toolName}`, {
requestId: result.requestId,
actor,
});
}
catch (err) {
// 区分 Gateway 自身错误 vs 权限拒绝
const errMsg = err instanceof Error ? err.message : String(err);
if (errMsg.startsWith('[PERMISSION_DENIED]') ||
errMsg.startsWith('[CONSTITUTION_VIOLATION]')) {
throw err;
}
// Gateway 内部故障不应阻断业务(降级放行 + 记录)
this.logger.error(`MCP Gateway gating error (degraded): ${toolName}`, { error: errMsg });
}
}
// ─── Lifecycle ────────────────────────────────────────
async start() {
await this.initialize();
// 首次 bootstrap 成功后的标记 → 注入 autoApprove(在连接建立前,安全写入 mcp.json)
const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
try {
applyPendingAutoApprove(projectRoot, this.logger);
}
catch {
/* non-blocking */
}
const transport = new StdioServerTransport();
await this.sdkServer.connect(transport);
const tierName = process.env.ASD_MCP_TIER || 'agent';
const maxTier = TIER_ORDER[tierName] ?? TIER_ORDER.agent;
const visibleCount = TOOLS.filter((t) => (TIER_ORDER[t.tier || 'agent'] ?? 0) <= maxTier).length;
this.logger.info(`MCP Server started (stdio) — ${visibleCount} tools [tier=${tierName}]`);
process.stderr.write(`AutoSnippet MCP ready — ${visibleCount} tools [tier=${tierName}]\n`);
}
async shutdown() {
if (this.sdkServer) {
await this.sdkServer.close();
}
if (this.bootstrap) {
await this.bootstrap.shutdown();
}
}
}
export async function startMcpServer() {
const server = new McpServer();
await server.start();
return server;
}
export default McpServer;