@johpaz/hive-sdk 0.0.14 → 0.0.15
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/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Hot Reload
|
|
3
|
+
*
|
|
4
|
+
* Watches for MCP server changes in DB and updates MCP Manager automatically
|
|
5
|
+
*
|
|
6
|
+
* Architecture: Direct Connection
|
|
7
|
+
* - MCP servers are tracked in DB (mcp_servers table)
|
|
8
|
+
* - MCP tools are loaded at runtime from connected servers (not stored in DB)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getDb } from "../storage/SQLiteStorage.ts";
|
|
12
|
+
import { logger } from "../utils/logger.ts";
|
|
13
|
+
import { decryptConfig } from "../storage/crypto.ts";
|
|
14
|
+
import { syncMCPToolsToDB, syncMCPToolsToFTS, clearMCPToolsFromDB } from "./MCPToolAdapter.ts";
|
|
15
|
+
import type { MCPClientManager } from "../mcp/index.ts";
|
|
16
|
+
|
|
17
|
+
const log = logger.child("mcp:hot-reload");
|
|
18
|
+
|
|
19
|
+
let _watchInterval: Timer | null = null;
|
|
20
|
+
let _lastKnownServers = new Set<string>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Start watching for MCP server changes
|
|
24
|
+
* Checks every 2 seconds for new/removed servers
|
|
25
|
+
*/
|
|
26
|
+
export function startMCPHotReload(mcpManager: MCPClientManager): void {
|
|
27
|
+
if (_watchInterval) {
|
|
28
|
+
log.warn("MCP Hot Reload already running");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log.info("Starting MCP Hot Reload watcher (2s interval)");
|
|
33
|
+
|
|
34
|
+
// Initial sync - sync all currently connected servers
|
|
35
|
+
syncMCPServers(mcpManager).then(() => {
|
|
36
|
+
log.info("Initial MCP server sync complete");
|
|
37
|
+
}).catch(err => {
|
|
38
|
+
log.error(`Initial MCP server sync failed: ${err.message}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Watch for changes
|
|
42
|
+
_watchInterval = setInterval(() => {
|
|
43
|
+
syncMCPServers(mcpManager);
|
|
44
|
+
}, 2000);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stop watching
|
|
49
|
+
*/
|
|
50
|
+
export function stopMCPHotReload(): void {
|
|
51
|
+
if (_watchInterval) {
|
|
52
|
+
clearInterval(_watchInterval);
|
|
53
|
+
_watchInterval = null;
|
|
54
|
+
log.info("MCP Hot Reload stopped");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sync MCP servers from DB to MCP Manager
|
|
60
|
+
* Note: Only server status is tracked, tools are loaded at runtime
|
|
61
|
+
*/
|
|
62
|
+
async function syncMCPServers(mcpManager: MCPClientManager): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
const db = getDb();
|
|
65
|
+
const dbServers = db.query(`SELECT * FROM mcp_servers WHERE enabled = 1`).all() as Record<string, any>[];
|
|
66
|
+
|
|
67
|
+
const currentServerNames = new Set(dbServers.map(s => s.id || s.name));
|
|
68
|
+
|
|
69
|
+
// Detect new servers
|
|
70
|
+
for (const server of dbServers) {
|
|
71
|
+
const serverName = server.id || server.name;
|
|
72
|
+
|
|
73
|
+
if (!_lastKnownServers.has(serverName)) {
|
|
74
|
+
log.info(`New MCP server detected: ${serverName} - connecting...`);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const mcpServerConfig: any = {
|
|
78
|
+
transport: server.transport,
|
|
79
|
+
command: server.command,
|
|
80
|
+
args: server.args ? JSON.parse(server.args) : [],
|
|
81
|
+
url: server.url,
|
|
82
|
+
enabled: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (server.headers_encrypted && server.headers_iv) {
|
|
86
|
+
mcpServerConfig.headers = decryptConfig(server.headers_encrypted, server.headers_iv);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update MCP Manager config (auto-connects new servers)
|
|
90
|
+
const currentConfig = (mcpManager as any).config || { servers: {} };
|
|
91
|
+
await mcpManager.updateConfig({
|
|
92
|
+
...currentConfig,
|
|
93
|
+
servers: {
|
|
94
|
+
...currentConfig.servers,
|
|
95
|
+
[serverName]: mcpServerConfig,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Wait a bit for connection to establish
|
|
100
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
101
|
+
|
|
102
|
+
// Get tools count and update status
|
|
103
|
+
const tools = mcpManager.getServerTools(serverName) || [];
|
|
104
|
+
db.query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ?`).run("connected", tools.length, serverName);
|
|
105
|
+
|
|
106
|
+
// Persist MCP tool definitions to DB and FTS5
|
|
107
|
+
// Use server.name (human-readable) for mcpToolId consistency with context-compiler
|
|
108
|
+
syncMCPToolsToDB(server.id || server.name, server.name || serverName, tools);
|
|
109
|
+
await syncMCPToolsToFTS();
|
|
110
|
+
|
|
111
|
+
log.info(`MCP server ${serverName} connected: ${tools.length} tools available`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log.error(`Failed to connect MCP server ${serverName}: ${(err as Error).message}`);
|
|
114
|
+
db.query(`UPDATE mcp_servers SET status = ? WHERE id = ?`).run("error", serverName);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Detect removed servers
|
|
120
|
+
for (const oldServerName of _lastKnownServers) {
|
|
121
|
+
if (!currentServerNames.has(oldServerName)) {
|
|
122
|
+
log.info(`MCP server removed: ${oldServerName} - disconnecting...`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Remove from MCP Manager
|
|
126
|
+
const currentConfig = (mcpManager as any).config || { servers: {} };
|
|
127
|
+
delete currentConfig.servers[oldServerName];
|
|
128
|
+
await mcpManager.updateConfig(currentConfig);
|
|
129
|
+
|
|
130
|
+
// Delete MCP tool definitions from DB and FTS5
|
|
131
|
+
clearMCPToolsFromDB(oldServerName);
|
|
132
|
+
|
|
133
|
+
// Update DB status
|
|
134
|
+
db.query(`UPDATE mcp_servers SET status = ?, tools_count = 0 WHERE id = ?`).run("disconnected", oldServerName);
|
|
135
|
+
|
|
136
|
+
log.info(`MCP server ${oldServerName} disconnected`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
log.error(`Failed to disconnect MCP server ${oldServerName}: ${(err as Error).message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_lastKnownServers = currentServerNames;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log.error(`MCP server sync failed: ${(err as Error).message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { MCPTool, MCPResource, MCPPrompt } from "./MCPClient.ts";
|
|
2
|
+
export { MCPClientManager } from "./MCPClient.ts";
|
|
3
|
+
export type { MCPToolDefinition } from "./MCPToolAdapter.ts";
|
|
4
|
+
export { mcpToolId, syncMCPToolsToDB, syncMCPToolsToFTS, clearMCPToolsFromDB } from "./MCPToolAdapter.ts";
|
|
5
|
+
export type { MCPConfig, MCPServerConfig } from "./config.ts";
|
|
6
|
+
export { setMCPManager, getMCPManager, hasMCPManager } from "./singleton.ts";
|
|
7
|
+
export { startMCPHotReload, stopMCPHotReload } from "./hot-reload.ts";
|
|
8
|
+
export type { LogLevel as MCPLogLevel, LogHandler as MCPLogHandler } from "./logger.ts";
|
|
9
|
+
export { logger as mcpLogger } from "./logger.ts";
|
|
10
|
+
export type { SSETransportConfig, WebSocketTransportConfig, StdioTransportConfig, TransportType, TransportOptions } from "./transports/index.ts";
|
|
11
|
+
export { SSETransport, WebSocketTransport, createTransport } from "./transports/index.ts";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export type LogHandler = (level: LogLevel, context: string, message: string, data?: Record<string, unknown>) => void;
|
|
3
|
+
|
|
4
|
+
class Logger {
|
|
5
|
+
private context: string;
|
|
6
|
+
private level: LogLevel = "info";
|
|
7
|
+
private handler: LogHandler | null = null;
|
|
8
|
+
|
|
9
|
+
constructor(context: string, handler: LogHandler | null = null) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.handler = handler;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setHandler(handler: LogHandler | null): void {
|
|
15
|
+
this.handler = handler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
|
|
19
|
+
if (this.handler) {
|
|
20
|
+
this.handler(level, this.context, message, data);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Silent by default if no handler is set, to avoid standard console pollution.
|
|
25
|
+
// The consumer (e.g., Hive Agent) should set a handler to bridge these logs.
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
debug(message: string, data?: Record<string, unknown>): void { this.log("debug", message, data); }
|
|
29
|
+
info(message: string, data?: Record<string, unknown>): void { this.log("info", message, data); }
|
|
30
|
+
warn(message: string, data?: Record<string, unknown>): void { this.log("warn", message, data); }
|
|
31
|
+
error(message: string, data?: Record<string, unknown>): void { this.log("error", message, data); }
|
|
32
|
+
|
|
33
|
+
child(context: string): Logger {
|
|
34
|
+
return new Logger(`${this.context}:${context}`, this.handler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setLevel(level: LogLevel): void {
|
|
38
|
+
this.level = level;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const logger = new Logger("mcp");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Manager Singleton
|
|
3
|
+
*
|
|
4
|
+
* Provides global access to the MCP Manager instance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MCPClientManager } from "../mcp/index.ts";
|
|
8
|
+
|
|
9
|
+
let _mcpManager: MCPClientManager | null = null;
|
|
10
|
+
|
|
11
|
+
export function setMCPManager(m: MCPClientManager): void {
|
|
12
|
+
_mcpManager = m;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getMCPManager(): MCPClientManager | null {
|
|
16
|
+
return _mcpManager;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasMCPManager(): boolean {
|
|
20
|
+
return _mcpManager !== null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
|
|
4
|
+
// CORRECCIÓN 1 — quitar extensión .ts de los imports
|
|
5
|
+
// Bun resuelve los módulos sin extensión correctamente
|
|
6
|
+
// Con .ts puede fallar en algunos contextos de build/bundle
|
|
7
|
+
import { SSETransport, type SSETransportConfig } from "./sse";
|
|
8
|
+
import { WebSocketTransport, type WebSocketTransportConfig } from "./websocket";
|
|
9
|
+
|
|
10
|
+
export { SSETransport, type SSETransportConfig };
|
|
11
|
+
export { WebSocketTransport, type WebSocketTransportConfig };
|
|
12
|
+
|
|
13
|
+
// CORRECCIÓN 2 — exportar StdioTransportConfig
|
|
14
|
+
// Estaba definido pero no exportado — el resto del código no puede importarlo
|
|
15
|
+
export interface StdioTransportConfig {
|
|
16
|
+
command: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TransportType = "stdio" | "sse" | "websocket";
|
|
22
|
+
|
|
23
|
+
export interface TransportOptions {
|
|
24
|
+
type: TransportType;
|
|
25
|
+
stdio?: StdioTransportConfig;
|
|
26
|
+
sse?: SSETransportConfig;
|
|
27
|
+
websocket?: WebSocketTransportConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createTransport(options: TransportOptions): Transport {
|
|
31
|
+
switch (options.type) {
|
|
32
|
+
case "stdio": {
|
|
33
|
+
if (!options.stdio) {
|
|
34
|
+
throw new Error("stdio config required for stdio transport");
|
|
35
|
+
}
|
|
36
|
+
return new StdioClientTransport({
|
|
37
|
+
command: options.stdio.command,
|
|
38
|
+
args: options.stdio.args ?? [],
|
|
39
|
+
env: options.stdio.env ?? (process.env as Record<string, string>),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case "sse": {
|
|
44
|
+
if (!options.sse) {
|
|
45
|
+
throw new Error("sse config required for SSE transport");
|
|
46
|
+
}
|
|
47
|
+
// CORRECCIÓN 3 — sin cast as unknown as Transport
|
|
48
|
+
// SSETransport ahora implementa Transport directamente (implements Transport)
|
|
49
|
+
// el cast doble era señal de que el tipo no estaba bien declarado en la clase
|
|
50
|
+
return new SSETransport(options.sse);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "websocket": {
|
|
54
|
+
if (!options.websocket) {
|
|
55
|
+
throw new Error("websocket config required for WebSocket transport");
|
|
56
|
+
}
|
|
57
|
+
// Igual — WebSocketTransport ahora implementa Transport directamente
|
|
58
|
+
return new WebSocketTransport(options.websocket);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
default: {
|
|
62
|
+
// exhaustive check — TypeScript avisa si falta un caso
|
|
63
|
+
const _exhaustive: never = options.type;
|
|
64
|
+
throw new Error(`Unknown transport type: ${_exhaustive}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
import { logger } from "../logger";
|
|
3
|
+
|
|
4
|
+
export interface SSETransportConfig {
|
|
5
|
+
url: string; // URL base
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class SSETransport implements Transport {
|
|
10
|
+
private baseUrl: string;
|
|
11
|
+
private messagesUrl: string | null = null; // Endpoint recibido del servidor
|
|
12
|
+
|
|
13
|
+
private headers: Record<string, string>;
|
|
14
|
+
private abortController: AbortController | null = null;
|
|
15
|
+
private cookies: string[] = [];
|
|
16
|
+
private startResolve: (() => void) | null = null;
|
|
17
|
+
private startReject: ((err: Error) => void) | null = null;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
|
|
20
|
+
onmessage: ((message: unknown) => void) | undefined;
|
|
21
|
+
onerror: ((error: Error) => void) | undefined;
|
|
22
|
+
onclose: (() => void) | undefined;
|
|
23
|
+
|
|
24
|
+
constructor(config: SSETransportConfig) {
|
|
25
|
+
this.baseUrl = config.url;
|
|
26
|
+
this.headers = config.headers ?? {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async start(): Promise<void> {
|
|
30
|
+
this.abortController = new AbortController();
|
|
31
|
+
|
|
32
|
+
return new Promise(async (resolve, reject) => {
|
|
33
|
+
// Timeout fallback: if no endpoint received in 5s, continue anyway
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
this.startResolve = null;
|
|
36
|
+
this.startReject = null;
|
|
37
|
+
resolve();
|
|
38
|
+
}, 5000);
|
|
39
|
+
|
|
40
|
+
this.startResolve = () => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
this.startResolve = null;
|
|
43
|
+
this.startReject = null;
|
|
44
|
+
resolve();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.startReject = (err) => {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
this.startResolve = null;
|
|
50
|
+
this.startReject = null;
|
|
51
|
+
reject(err);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
logger.debug(`[SSE] Connecting to: ${this.baseUrl}`);
|
|
56
|
+
const response = await fetch(this.baseUrl, {
|
|
57
|
+
method: "GET",
|
|
58
|
+
headers: {
|
|
59
|
+
"Accept": "application/json, text/event-stream",
|
|
60
|
+
"Cache-Control": "no-cache",
|
|
61
|
+
...this.headers,
|
|
62
|
+
},
|
|
63
|
+
signal: this.abortController!.signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (response.status === 405) {
|
|
67
|
+
logger.debug(`[SSE] GET not allowed (405), falling back to Streamable HTTP pattern for ${this.baseUrl}`);
|
|
68
|
+
this.startResolve();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
this.startReject(new Error(`MCP SSE connection failed: ${response.status} ${response.statusText}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.readSessionId(response);
|
|
78
|
+
|
|
79
|
+
if (response.body) {
|
|
80
|
+
this.startReading(response.body);
|
|
81
|
+
} else {
|
|
82
|
+
this.startResolve();
|
|
83
|
+
}
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
if (error.name !== "AbortError") {
|
|
86
|
+
this.startReject(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private readSessionId(response: Response) {
|
|
93
|
+
const sessionId = response.headers.get("x-session-id") ??
|
|
94
|
+
response.headers.get("mcp-session-id");
|
|
95
|
+
if (sessionId) {
|
|
96
|
+
this.sessionId = sessionId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Track cookies for session affinity (important for n8n/proxies)
|
|
100
|
+
const setCookie = response.headers.get("set-cookie");
|
|
101
|
+
if (setCookie) {
|
|
102
|
+
// Simple cookie extraction: just keep the keys and values
|
|
103
|
+
const newCookies = setCookie.split(',').map(c => c.split(';')[0].trim());
|
|
104
|
+
this.cookies = [...new Set([...this.cookies, ...newCookies])];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private startReading(stream: ReadableStream<Uint8Array>) {
|
|
109
|
+
const reader = stream.getReader();
|
|
110
|
+
const decoder = new TextDecoder();
|
|
111
|
+
let buffer = "";
|
|
112
|
+
|
|
113
|
+
this.processStream(reader, decoder, buffer).catch((error) => {
|
|
114
|
+
if (this.onerror && error.name !== "AbortError") {
|
|
115
|
+
this.onerror(error instanceof Error ? error : new Error(String(error)));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async processStream(
|
|
121
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
122
|
+
decoder: TextDecoder,
|
|
123
|
+
buffer: string
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
let eventType = "message";
|
|
127
|
+
let eventData = "";
|
|
128
|
+
|
|
129
|
+
while (true) {
|
|
130
|
+
const { done, value } = await reader.read();
|
|
131
|
+
|
|
132
|
+
if (done) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
buffer += decoder.decode(value, { stream: true });
|
|
137
|
+
|
|
138
|
+
const lines = buffer.split("\n");
|
|
139
|
+
buffer = lines.pop() ?? "";
|
|
140
|
+
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
if (line.startsWith("event: ")) {
|
|
143
|
+
eventType = line.slice(7).trim();
|
|
144
|
+
} else if (line.startsWith("data: ")) {
|
|
145
|
+
const data = line.slice(6);
|
|
146
|
+
if (data.trim() === "[DONE]") {
|
|
147
|
+
this.onclose?.();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
eventData += data + "\n";
|
|
151
|
+
} else if (line === "") {
|
|
152
|
+
if (eventData) {
|
|
153
|
+
eventData = eventData.trim();
|
|
154
|
+
if (eventType === "endpoint") {
|
|
155
|
+
try {
|
|
156
|
+
this.messagesUrl = new URL(eventData, this.baseUrl).href;
|
|
157
|
+
logger.debug(`[SSE] Messages endpoint received: ${this.messagesUrl}`);
|
|
158
|
+
this.startResolve?.();
|
|
159
|
+
} catch (e) {
|
|
160
|
+
logger.warn(`[SSE] Failed to parse endpoint: ${eventData}`);
|
|
161
|
+
}
|
|
162
|
+
} else if (eventType === "message" || eventType === "") {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(eventData);
|
|
165
|
+
this.onmessage?.(parsed);
|
|
166
|
+
} catch {
|
|
167
|
+
// Ignorar heartbeats o no-JSON
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
eventData = "";
|
|
171
|
+
}
|
|
172
|
+
eventType = "message";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
if (error.name !== "AbortError") {
|
|
178
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async close(): Promise<void> {
|
|
184
|
+
this.abortController?.abort();
|
|
185
|
+
this.abortController = null;
|
|
186
|
+
this.sessionId = undefined;
|
|
187
|
+
this.onclose?.();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async send(message: unknown): Promise<void> {
|
|
191
|
+
if (!this.abortController) {
|
|
192
|
+
throw new Error("SSE transport not started — llama start() primero");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const targetUrl = this.messagesUrl || this.baseUrl;
|
|
196
|
+
let url = targetUrl;
|
|
197
|
+
|
|
198
|
+
if (this.sessionId && !url.includes(`sessionId=${this.sessionId}`)) {
|
|
199
|
+
url = `${targetUrl}${targetUrl.includes('?') ? '&' : '?'}sessionId=${this.sessionId}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const response = await fetch(url, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
"Accept": "application/json, text/event-stream",
|
|
207
|
+
...this.headers,
|
|
208
|
+
...(this.cookies.length > 0 ? { "Cookie": this.cookies.join('; ') } : {}),
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify(message),
|
|
211
|
+
signal: this.abortController.signal,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.readSessionId(response);
|
|
215
|
+
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
const body = await response.text().catch(() => "");
|
|
218
|
+
throw new Error(`MCP message failed (${response.status}): ${body || response.statusText}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
222
|
+
|
|
223
|
+
if (contentType.includes("text/event-stream") && response.body) {
|
|
224
|
+
this.startReading(response.body);
|
|
225
|
+
}
|
|
226
|
+
else if (contentType.includes("application/json")) {
|
|
227
|
+
const text = await response.text();
|
|
228
|
+
if (text.trim()) {
|
|
229
|
+
try {
|
|
230
|
+
this.onmessage?.(JSON.parse(text));
|
|
231
|
+
} catch {
|
|
232
|
+
// ignored
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function createSSETransport(config: SSETransportConfig): Transport {
|
|
240
|
+
return new SSETransport(config) as unknown as Transport;
|
|
241
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
|
|
3
|
+
export interface WebSocketTransportConfig {
|
|
4
|
+
url: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
reconnect?: boolean; // reconectar automáticamente si se cae (default: true)
|
|
7
|
+
reconnectDelay?: number; // ms entre intentos de reconexión (default: 3000)
|
|
8
|
+
reconnectMaxAttempts?: number; // máximo de intentos (default: 10)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class WebSocketTransport implements Transport {
|
|
12
|
+
private url: string;
|
|
13
|
+
private ws: WebSocket | null = null;
|
|
14
|
+
private headers?: Record<string, string>;
|
|
15
|
+
private intentionallyClosed = false; // distingue close() manual de caída
|
|
16
|
+
private reconnectAttempts = 0;
|
|
17
|
+
private reconnectDelay: number;
|
|
18
|
+
private reconnectMaxAttempts: number;
|
|
19
|
+
private shouldReconnect: boolean;
|
|
20
|
+
|
|
21
|
+
onmessage: ((message: unknown) => void) | undefined;
|
|
22
|
+
onerror: ((error: Error) => void) | undefined;
|
|
23
|
+
onclose: (() => void) | undefined;
|
|
24
|
+
|
|
25
|
+
constructor(config: WebSocketTransportConfig) {
|
|
26
|
+
this.url = config.url;
|
|
27
|
+
this.headers = config.headers;
|
|
28
|
+
this.shouldReconnect = config.reconnect ?? true;
|
|
29
|
+
this.reconnectDelay = config.reconnectDelay ?? 3000;
|
|
30
|
+
this.reconnectMaxAttempts = config.reconnectMaxAttempts ?? 10;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(): Promise<void> {
|
|
34
|
+
this.intentionallyClosed = false;
|
|
35
|
+
this.reconnectAttempts = 0;
|
|
36
|
+
return this.connect();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private connect(): Promise<void> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
|
|
42
|
+
// CORRECCIÓN 1 — headers en Bun WebSocket
|
|
43
|
+
// Bun acepta las opciones como segundo argumento cuando no hay subprotocols,
|
|
44
|
+
// o como objeto con `headers` dentro de un array de subprotocols vacío.
|
|
45
|
+
// La forma más segura y compatible:
|
|
46
|
+
const ws = this.headers && Object.keys(this.headers).length > 0
|
|
47
|
+
? new WebSocket(this.url, {
|
|
48
|
+
// @ts-expect-error — Bun extiende la API estándar de WebSocket
|
|
49
|
+
headers: this.headers,
|
|
50
|
+
})
|
|
51
|
+
: new WebSocket(this.url);
|
|
52
|
+
|
|
53
|
+
this.ws = ws;
|
|
54
|
+
let resolved = false;
|
|
55
|
+
|
|
56
|
+
ws.onopen = () => {
|
|
57
|
+
resolved = true;
|
|
58
|
+
this.reconnectAttempts = 0; // reset contador al conectar exitosamente
|
|
59
|
+
resolve();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(event.data as string);
|
|
65
|
+
this.onmessage?.(data);
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignorar mensajes no-JSON (ping/pong, heartbeats)
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// CORRECCIÓN 2 — separar el error de conexión del error post-conexión
|
|
72
|
+
ws.onerror = (event: Event) => {
|
|
73
|
+
const error =
|
|
74
|
+
(event as ErrorEvent).error ??
|
|
75
|
+
new Error(`WebSocket error en ${this.url}`);
|
|
76
|
+
|
|
77
|
+
if (!resolved) {
|
|
78
|
+
// Error durante la conexión inicial → rechazar la promesa
|
|
79
|
+
reject(error);
|
|
80
|
+
} else {
|
|
81
|
+
// Error después de conectar → notificar sin rechazar
|
|
82
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// CORRECCIÓN 3 — reconexión automática en cierre inesperado
|
|
87
|
+
ws.onclose = (event: CloseEvent) => {
|
|
88
|
+
this.ws = null;
|
|
89
|
+
|
|
90
|
+
if (this.intentionallyClosed) {
|
|
91
|
+
// Cierre manual con close() — notificar y no reconectar
|
|
92
|
+
this.onclose?.();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!resolved) {
|
|
97
|
+
// Cierre antes de que se abriera → rechazar la promesa
|
|
98
|
+
reject(new Error(
|
|
99
|
+
`WebSocket cerrado antes de conectar — code: ${event.code}, reason: ${event.reason}`
|
|
100
|
+
));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Cierre inesperado después de conectar
|
|
105
|
+
if (
|
|
106
|
+
this.shouldReconnect &&
|
|
107
|
+
this.reconnectAttempts < this.reconnectMaxAttempts
|
|
108
|
+
) {
|
|
109
|
+
this.reconnectAttempts++;
|
|
110
|
+
const delay = this.reconnectDelay * this.reconnectAttempts; // backoff lineal
|
|
111
|
+
|
|
112
|
+
setTimeout(async () => {
|
|
113
|
+
try {
|
|
114
|
+
await this.connect();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this.onerror?.(err instanceof Error ? err : new Error(String(err)));
|
|
117
|
+
this.onclose?.();
|
|
118
|
+
}
|
|
119
|
+
}, delay);
|
|
120
|
+
} else {
|
|
121
|
+
// Sin más intentos → notificar cierre definitivo
|
|
122
|
+
this.onerror?.(new Error(
|
|
123
|
+
`WebSocket desconectado después de ${this.reconnectAttempts} intentos — ${this.url}`
|
|
124
|
+
));
|
|
125
|
+
this.onclose?.();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async close(): Promise<void> {
|
|
132
|
+
this.intentionallyClosed = true; // marcar como cierre intencional
|
|
133
|
+
if (this.ws) {
|
|
134
|
+
// Código 1000 = cierre normal
|
|
135
|
+
this.ws.close(1000, "Client closed connection");
|
|
136
|
+
this.ws = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async send(message: unknown): Promise<void> {
|
|
141
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`WebSocket no está conectado — readyState: ${this.ws?.readyState ?? "null"}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
this.ws.send(JSON.stringify(message));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Útil para health checks desde el MCP client
|
|
150
|
+
get isConnected(): boolean {
|
|
151
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createWebSocketTransport(
|
|
156
|
+
config: WebSocketTransportConfig
|
|
157
|
+
): Transport {
|
|
158
|
+
return new WebSocketTransport(config) as unknown as Transport;
|
|
159
|
+
}
|