@meowlynxsea/koi 0.1.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.
- package/LICENSE +34 -0
- package/NOTICE +35 -0
- package/README.md +15 -0
- package/bin/koi +12 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/main.js +489918 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +51 -0
- package/src/agent/check-permissions.ts +239 -0
- package/src/agent/hooks/message-utils.ts +305 -0
- package/src/agent/hooks/types.ts +32 -0
- package/src/agent/hooks.ts +1560 -0
- package/src/agent/mode.ts +163 -0
- package/src/agent/monitor-registry.ts +308 -0
- package/src/agent/permission-ui.ts +71 -0
- package/src/agent/plan-ui.ts +74 -0
- package/src/agent/question-ui.ts +58 -0
- package/src/agent/session-fork.ts +299 -0
- package/src/agent/session-snapshots.ts +216 -0
- package/src/agent/session-store.ts +649 -0
- package/src/agent/session-tasks.ts +305 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +176 -0
- package/src/agent/subagent.ts +194 -0
- package/src/agent/tool-orchestration.ts +55 -0
- package/src/agent/tools.ts +8 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/commands.ts +5 -0
- package/src/commands/skills/index.ts +23 -0
- package/src/config/models.ts +6 -0
- package/src/config/settings.ts +392 -0
- package/src/main.tsx +64 -0
- package/src/services/mcp/client.ts +194 -0
- package/src/services/mcp/config.ts +232 -0
- package/src/services/mcp/connection-manager.ts +258 -0
- package/src/services/mcp/index.ts +80 -0
- package/src/services/mcp/mcp-commands.ts +114 -0
- package/src/services/mcp/stdio-transport.ts +246 -0
- package/src/services/mcp/types.ts +155 -0
- package/src/skills/SkillsMenu.tsx +370 -0
- package/src/skills/bundled/batch.ts +106 -0
- package/src/skills/bundled/debug.ts +86 -0
- package/src/skills/bundled/loremIpsum.ts +101 -0
- package/src/skills/bundled/remember.ts +97 -0
- package/src/skills/bundled/simplify.ts +100 -0
- package/src/skills/bundled/skillify.ts +123 -0
- package/src/skills/bundled/stuck.ts +101 -0
- package/src/skills/bundled/updateConfig.ts +228 -0
- package/src/skills/bundled.ts +46 -0
- package/src/skills/frontmatter.ts +179 -0
- package/src/skills/index.ts +87 -0
- package/src/skills/invoke.ts +231 -0
- package/src/skills/loader.ts +710 -0
- package/src/skills/substitution.ts +169 -0
- package/src/skills/types.ts +201 -0
- package/src/tools/agent.ts +143 -0
- package/src/tools/ask-user-question.ts +46 -0
- package/src/tools/bash.ts +148 -0
- package/src/tools/edit.ts +164 -0
- package/src/tools/glob.ts +102 -0
- package/src/tools/grep.ts +248 -0
- package/src/tools/index.ts +73 -0
- package/src/tools/list-mcp-resources.ts +74 -0
- package/src/tools/ls.ts +85 -0
- package/src/tools/mcp.ts +76 -0
- package/src/tools/monitor.ts +159 -0
- package/src/tools/plan-mode.ts +134 -0
- package/src/tools/read-mcp-resource.ts +79 -0
- package/src/tools/read.ts +137 -0
- package/src/tools/skill.ts +176 -0
- package/src/tools/task.ts +349 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/webfetch-domains.ts +239 -0
- package/src/tools/webfetch.ts +533 -0
- package/src/tools/write.ts +101 -0
- package/src/tui/app.tsx +1178 -0
- package/src/tui/components/chat-panel.tsx +1071 -0
- package/src/tui/components/command-panel.tsx +261 -0
- package/src/tui/components/confirm-modal.tsx +135 -0
- package/src/tui/components/connect-modal.tsx +435 -0
- package/src/tui/components/connecting-modal.tsx +167 -0
- package/src/tui/components/edit-pending-modal.tsx +103 -0
- package/src/tui/components/exit-modal.tsx +131 -0
- package/src/tui/components/fork-modal.tsx +377 -0
- package/src/tui/components/image-preview-modal.tsx +141 -0
- package/src/tui/components/image-utils.ts +128 -0
- package/src/tui/components/info-bar.tsx +103 -0
- package/src/tui/components/input-box.tsx +352 -0
- package/src/tui/components/mcp/MCPSettings.tsx +386 -0
- package/src/tui/components/mcp/index.ts +7 -0
- package/src/tui/components/model-modal.tsx +310 -0
- package/src/tui/components/pending-area.tsx +88 -0
- package/src/tui/components/rename-modal.tsx +119 -0
- package/src/tui/components/session-modal.tsx +233 -0
- package/src/tui/components/side-bar.tsx +349 -0
- package/src/tui/components/tool-output.ts +6 -0
- package/src/tui/hooks/user-prompt-history.ts +114 -0
- package/src/tui/theme.ts +63 -0
- package/src/types/commands.ts +80 -0
- package/src/types/cross-spawn.d.ts +24 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Services - Koi Agent
|
|
3
|
+
*
|
|
4
|
+
* Model Context Protocol integration for Koi Agent.
|
|
5
|
+
* Supports stdio, SSE, HTTP, and WebSocket transport types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
export {
|
|
13
|
+
setMcpConfig,
|
|
14
|
+
getMcpConfig,
|
|
15
|
+
getAllMcpConfigs,
|
|
16
|
+
getMcpConfigsByScope,
|
|
17
|
+
removeMcpConfig,
|
|
18
|
+
isMcpServerDisabled,
|
|
19
|
+
setMcpServerEnabled,
|
|
20
|
+
getMcpServerNames,
|
|
21
|
+
loadMcpConfigs,
|
|
22
|
+
loadProjectMcpConfig,
|
|
23
|
+
loadLocalMcpConfig,
|
|
24
|
+
validateMcpConfig,
|
|
25
|
+
exportMcpConfigs,
|
|
26
|
+
importMcpConfigs,
|
|
27
|
+
} from "./config.js";
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
McpConfigValidationResult,
|
|
31
|
+
McpConfigExport,
|
|
32
|
+
} from "./config.js";
|
|
33
|
+
|
|
34
|
+
// Client
|
|
35
|
+
export {
|
|
36
|
+
connectToServer,
|
|
37
|
+
disconnectFromServer,
|
|
38
|
+
callMcpTool,
|
|
39
|
+
listMcpResources,
|
|
40
|
+
readMcpResource,
|
|
41
|
+
listMcpPrompts,
|
|
42
|
+
executeMcpPrompt,
|
|
43
|
+
} from "./client.js";
|
|
44
|
+
|
|
45
|
+
export type { ConnectResult } from "./client.js";
|
|
46
|
+
|
|
47
|
+
// Connection Manager
|
|
48
|
+
export {
|
|
49
|
+
initializeMcpConnections,
|
|
50
|
+
connectMcpServer,
|
|
51
|
+
disconnectMcpServer,
|
|
52
|
+
reconnectMcpServer,
|
|
53
|
+
toggleMcpServer,
|
|
54
|
+
getMcpConnections,
|
|
55
|
+
getMcpConnection,
|
|
56
|
+
getConnectedServers,
|
|
57
|
+
getAllMcpTools,
|
|
58
|
+
getAllMcpResources,
|
|
59
|
+
getMcpStatusSummary,
|
|
60
|
+
isMcpConnecting,
|
|
61
|
+
getMcpError,
|
|
62
|
+
disconnectAllMcpServers,
|
|
63
|
+
resetMcpConnectionManager,
|
|
64
|
+
type McpConnectionProgress,
|
|
65
|
+
type McpProgressCallback,
|
|
66
|
+
} from "./connection-manager.js";
|
|
67
|
+
|
|
68
|
+
// Stdio Transport with JSON filtering
|
|
69
|
+
export { FilteredStdioClientTransport } from "./stdio-transport.js";
|
|
70
|
+
|
|
71
|
+
// Commands
|
|
72
|
+
export {
|
|
73
|
+
addMcpServer,
|
|
74
|
+
removeMcpServer,
|
|
75
|
+
enableMcpServer,
|
|
76
|
+
disableMcpServer,
|
|
77
|
+
refreshMcpServer,
|
|
78
|
+
getMcpServerInfo,
|
|
79
|
+
parseMcpArgs,
|
|
80
|
+
} from "./mcp-commands.js";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Command Support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ScopedMcpConfig } from "./types.js";
|
|
6
|
+
import { getMcpConfig, setMcpConfig, removeMcpConfig, isMcpServerDisabled, setMcpServerEnabled, validateMcpConfig } from "./config.js";
|
|
7
|
+
import { connectMcpServer, disconnectMcpServer, reconnectMcpServer, getMcpConnections } from "./connection-manager.js";
|
|
8
|
+
|
|
9
|
+
export async function addMcpServer(name: string, config: Partial<ScopedMcpConfig>): Promise<{ success: boolean; error?: string; warning?: string }> {
|
|
10
|
+
const validation = validateMcpConfig(name, config);
|
|
11
|
+
if (!validation.valid) {
|
|
12
|
+
return { success: false, error: validation.errors?.join(", ") ?? "Invalid configuration" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const existing = getMcpConfig(name);
|
|
16
|
+
if (existing) {
|
|
17
|
+
return { success: false, error: `Server '${name}' already exists` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setMcpConfig(name, config, "user");
|
|
21
|
+
const result = await connectMcpServer(name);
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
return { success: true, warning: `Server added but failed to connect: ${result.error}` };
|
|
24
|
+
}
|
|
25
|
+
return { success: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function removeMcpServer(name: string): { success: boolean; error?: string } {
|
|
29
|
+
const config = getMcpConfig(name);
|
|
30
|
+
if (!config) return { success: false, error: `Server '${name}' not found` };
|
|
31
|
+
disconnectMcpServer(name).catch(() => {});
|
|
32
|
+
removeMcpConfig(name);
|
|
33
|
+
return { success: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function enableMcpServer(name: string): { success: boolean; error?: string } {
|
|
37
|
+
const config = getMcpConfig(name);
|
|
38
|
+
if (!config) return { success: false, error: `Server '${name}' not found` };
|
|
39
|
+
setMcpServerEnabled(name, true);
|
|
40
|
+
return { success: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function disableMcpServer(name: string): { success: boolean; error?: string } {
|
|
44
|
+
const config = getMcpConfig(name);
|
|
45
|
+
if (!config) return { success: false, error: `Server '${name}' not found` };
|
|
46
|
+
setMcpServerEnabled(name, false);
|
|
47
|
+
disconnectMcpServer(name).catch(() => {});
|
|
48
|
+
return { success: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function refreshMcpServer(name: string, options?: { onProgress?: (message: string) => void }): Promise<{ success: boolean; error?: string }> {
|
|
52
|
+
const config = getMcpConfig(name);
|
|
53
|
+
if (!config) return { success: false, error: `Server '${name}' not found` };
|
|
54
|
+
options?.onProgress?.(`Reconnecting to ${name}...`);
|
|
55
|
+
const result = await reconnectMcpServer(name, { onProgress: options?.onProgress });
|
|
56
|
+
if (!result.success) return { success: false, error: result.error };
|
|
57
|
+
return { success: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getMcpServerInfo(name: string): {
|
|
61
|
+
name: string;
|
|
62
|
+
config?: Partial<ScopedMcpConfig>;
|
|
63
|
+
status: string;
|
|
64
|
+
toolCount?: number;
|
|
65
|
+
resourceCount?: number;
|
|
66
|
+
error?: string;
|
|
67
|
+
} {
|
|
68
|
+
const config = getMcpConfig(name);
|
|
69
|
+
if (!config) return { name, status: "not_found" };
|
|
70
|
+
const connection = getMcpConnections().get(name);
|
|
71
|
+
const status = connection?.status ?? (isMcpServerDisabled(name) ? "disabled" : "disconnected");
|
|
72
|
+
const connectedConn = connection?.status === "connected" ? connection : null;
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
config,
|
|
76
|
+
status,
|
|
77
|
+
toolCount: connectedConn?.tools?.length,
|
|
78
|
+
resourceCount: connectedConn?.resources?.length,
|
|
79
|
+
error: connection?.status === "failed" ? (connection as { error?: string }).error : undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseMcpArgs(args: string): {
|
|
84
|
+
name?: string;
|
|
85
|
+
type?: "stdio" | "sse" | "http" | "ws";
|
|
86
|
+
command?: string;
|
|
87
|
+
url?: string;
|
|
88
|
+
args?: string[];
|
|
89
|
+
} {
|
|
90
|
+
const result: { name?: string; type?: "stdio" | "sse" | "http" | "ws"; command?: string; url?: string; args?: string[] } = {};
|
|
91
|
+
const parts = args.trim().split(/\s+/);
|
|
92
|
+
let i = 0;
|
|
93
|
+
while (i < parts.length) {
|
|
94
|
+
const part = parts[i]!;
|
|
95
|
+
if (part === "--stdio" || part === "-s") { result.type = "stdio"; i++; }
|
|
96
|
+
else if (part === "--sse" || part === "-e") { result.type = "sse"; i++; }
|
|
97
|
+
else if (part === "--http" || part === "-h") { result.type = "http"; i++; }
|
|
98
|
+
else if (part === "--ws" || part === "-w") { result.type = "ws"; i++; }
|
|
99
|
+
else if (part === "--name" || part === "-n") { result.name = parts[++i]; i++; }
|
|
100
|
+
else if (part.startsWith("-")) { i++; }
|
|
101
|
+
else if (!result.name && !result.command && !result.url) {
|
|
102
|
+
if (part.includes("://")) { result.url = part; }
|
|
103
|
+
else { result.command = part; }
|
|
104
|
+
i++;
|
|
105
|
+
} else if (result.command || result.url) {
|
|
106
|
+
result.args = result.args ?? [];
|
|
107
|
+
result.args.push(part);
|
|
108
|
+
i++;
|
|
109
|
+
} else {
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Stdio Transport with JSON Filtering
|
|
3
|
+
*
|
|
4
|
+
* Some MCP servers (like context7) output non-JSON text to stdout on startup,
|
|
5
|
+
* which would cause parsing errors. This wrapper filters out non-JSON lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import spawn from "cross-spawn";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import { getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
11
|
+
import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
12
|
+
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
|
|
14
|
+
// Transport interface for FilteredStdioClientTransport
|
|
15
|
+
interface Transport {
|
|
16
|
+
onclose?: () => void;
|
|
17
|
+
onerror?: (error: Error) => void;
|
|
18
|
+
onmessage?: (message: JSONRPCMessage) => void;
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
readonly pid?: number | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Inline serializeMessage to avoid internal path imports
|
|
26
|
+
function serializeMessage(message: JSONRPCMessage): string {
|
|
27
|
+
return JSON.stringify(message) + "\n";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Buffers a continuous stdio stream into discrete JSON-RPC messages.
|
|
32
|
+
*/
|
|
33
|
+
class ReadBuffer {
|
|
34
|
+
private _buffer?: Buffer;
|
|
35
|
+
|
|
36
|
+
append(chunk: Buffer): void {
|
|
37
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
readMessage(): { line: string; remaining: Buffer | undefined } | null {
|
|
41
|
+
if (!this._buffer) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const index = this._buffer.indexOf("\n");
|
|
45
|
+
if (index === -1) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
|
|
49
|
+
const remaining = this._buffer.subarray(index + 1);
|
|
50
|
+
this._buffer = remaining.length > 0 ? remaining : undefined;
|
|
51
|
+
return { line, remaining };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear(): void {
|
|
55
|
+
this._buffer = undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidJsonRpc(line: string): boolean {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed.startsWith("{")) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
66
|
+
return typeof parsed === "object" && parsed !== null && "jsonrpc" in (parsed as Record<string, unknown>);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Client transport for stdio with JSON filtering.
|
|
74
|
+
* Filters out non-JSON startup messages from MCP servers.
|
|
75
|
+
*/
|
|
76
|
+
export class FilteredStdioClientTransport implements Transport {
|
|
77
|
+
private _process?: ReturnType<typeof spawn>;
|
|
78
|
+
private _readBuffer: ReadBuffer;
|
|
79
|
+
private _serverParams: StdioServerParameters;
|
|
80
|
+
|
|
81
|
+
onclose?: () => void;
|
|
82
|
+
onerror?: (error: Error) => void;
|
|
83
|
+
onmessage?: (message: JSONRPCMessage) => void;
|
|
84
|
+
|
|
85
|
+
constructor(server: StdioServerParameters) {
|
|
86
|
+
this._readBuffer = new ReadBuffer();
|
|
87
|
+
this._serverParams = server;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async start(): Promise<void> {
|
|
91
|
+
if (this._process) {
|
|
92
|
+
throw new Error("StdioClientTransport already started!");
|
|
93
|
+
}
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
// Always pipe stderr so we can capture and suppress startup messages
|
|
96
|
+
// Only inherit stderr explicitly if user requests it
|
|
97
|
+
const stderrMode: "pipe" | "inherit" | "ignore" =
|
|
98
|
+
this._serverParams.stderr === "inherit" ? "inherit" :
|
|
99
|
+
this._serverParams.stderr === "ignore" ? "ignore" :
|
|
100
|
+
"pipe";
|
|
101
|
+
|
|
102
|
+
this._process = spawn(this._serverParams.command, this._serverParams.args ?? [], {
|
|
103
|
+
env: {
|
|
104
|
+
...getDefaultEnvironment(),
|
|
105
|
+
...this._serverParams.env,
|
|
106
|
+
},
|
|
107
|
+
stdio: ["pipe", "pipe", stderrMode],
|
|
108
|
+
shell: false,
|
|
109
|
+
windowsHide: process.platform === "win32",
|
|
110
|
+
cwd: this._serverParams.cwd,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this._process.on("error", (error: Error) => {
|
|
114
|
+
reject(error);
|
|
115
|
+
this.onerror?.(error);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this._process.on("spawn", () => {
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this._process.on("close", (_code: number) => {
|
|
123
|
+
this._process = undefined;
|
|
124
|
+
this.onclose?.();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this._process.stdin?.on("error", (error: Error) => {
|
|
128
|
+
this.onerror?.(error);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this._process.stdout?.on("data", (chunk: Buffer) => {
|
|
132
|
+
this._readBuffer.append(chunk);
|
|
133
|
+
this.processReadBuffer();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this._process.stdout?.on("error", (error: Error) => {
|
|
137
|
+
this.onerror?.(error);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Capture stderr but don't output it to console
|
|
141
|
+
// This filters out server startup messages like "Server v2.2.4 running on stdio"
|
|
142
|
+
if (this._process.stderr) {
|
|
143
|
+
this._process.stderr.on("data", (_chunk: Buffer) => {
|
|
144
|
+
// Silently consume stderr to prevent startup messages from appearing in TUI
|
|
145
|
+
// Only emit actual errors if needed
|
|
146
|
+
});
|
|
147
|
+
this._process.stderr.on("error", () => {
|
|
148
|
+
// Ignore stderr errors
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get pid(): number | null {
|
|
155
|
+
return this._process?.pid ?? null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private processReadBuffer(): void {
|
|
159
|
+
// Filter out non-JSON lines
|
|
160
|
+
while (true) {
|
|
161
|
+
const result = this._readBuffer.readMessage();
|
|
162
|
+
if (result === null) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { line } = result;
|
|
167
|
+
|
|
168
|
+
// Skip empty lines
|
|
169
|
+
if (!line.trim()) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Skip non-JSON lines (like server startup messages)
|
|
174
|
+
if (!isValidJsonRpc(line)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const message = JSON.parse(line) as JSONRPCMessage;
|
|
180
|
+
this.onmessage?.(message);
|
|
181
|
+
} catch {
|
|
182
|
+
// Skip invalid JSON-RPC messages
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async close(): Promise<void> {
|
|
189
|
+
if (this._process) {
|
|
190
|
+
const processToClose = this._process;
|
|
191
|
+
this._process = undefined;
|
|
192
|
+
const closePromise = new Promise<void>((resolve) => {
|
|
193
|
+
processToClose.once("close", () => {
|
|
194
|
+
resolve();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
processToClose.stdin?.end();
|
|
200
|
+
} catch {
|
|
201
|
+
// ignore
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await Promise.race([
|
|
205
|
+
closePromise,
|
|
206
|
+
new Promise<void>((resolve) => setTimeout(resolve, 2000).unref()),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
if (processToClose.exitCode === null) {
|
|
210
|
+
try {
|
|
211
|
+
processToClose.kill("SIGTERM");
|
|
212
|
+
} catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
await Promise.race([
|
|
216
|
+
closePromise,
|
|
217
|
+
new Promise<void>((resolve) => setTimeout(resolve, 2000).unref()),
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (processToClose.exitCode === null) {
|
|
222
|
+
try {
|
|
223
|
+
processToClose.kill("SIGKILL");
|
|
224
|
+
} catch {
|
|
225
|
+
// ignore
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this._readBuffer.clear();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
send(message: JSONRPCMessage): Promise<void> {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
if (!this._process?.stdin) {
|
|
235
|
+
reject(new Error("Not connected"));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const json = serializeMessage(message);
|
|
239
|
+
if (this._process.stdin.write(json)) {
|
|
240
|
+
resolve();
|
|
241
|
+
} else {
|
|
242
|
+
this._process.stdin.once("drain", resolve);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Types for Koi Agent
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import type { Tool, Resource, ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
|
|
8
|
+
// Transport Types
|
|
9
|
+
export const TransportSchema = ["stdio", "sse", "http", "ws"] as const;
|
|
10
|
+
export type Transport = (typeof TransportSchema)[number];
|
|
11
|
+
|
|
12
|
+
// Configuration Types - Simple flat structure
|
|
13
|
+
export interface McpServerConfig {
|
|
14
|
+
type?: "stdio" | "sse" | "http" | "ws";
|
|
15
|
+
command?: string;
|
|
16
|
+
args?: string[];
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
url?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
authToken?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Config Scope
|
|
24
|
+
export type ConfigScope = "user" | "project" | "local";
|
|
25
|
+
|
|
26
|
+
export interface ScopedMcpConfig extends McpServerConfig {
|
|
27
|
+
scope: ConfigScope;
|
|
28
|
+
description?: string;
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Config File Format
|
|
33
|
+
export interface McpJsonConfig {
|
|
34
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Connection State Types
|
|
38
|
+
export type ConnectionStatus = "connected" | "failed" | "pending" | "disabled" | "disconnected";
|
|
39
|
+
|
|
40
|
+
export interface ConnectedMCPServer {
|
|
41
|
+
client: Client;
|
|
42
|
+
name: string;
|
|
43
|
+
status: "connected";
|
|
44
|
+
capabilities: ServerCapabilities;
|
|
45
|
+
serverInfo?: { name: string; version: string };
|
|
46
|
+
instructions?: string;
|
|
47
|
+
config: ScopedMcpConfig;
|
|
48
|
+
tools: Tool[];
|
|
49
|
+
resources: Resource[];
|
|
50
|
+
cleanup: () => Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface FailedMCPServer {
|
|
54
|
+
name: string;
|
|
55
|
+
status: "failed";
|
|
56
|
+
config: ScopedMcpConfig;
|
|
57
|
+
error?: string;
|
|
58
|
+
lastAttempt?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PendingMCPServer {
|
|
62
|
+
name: string;
|
|
63
|
+
status: "pending";
|
|
64
|
+
config: ScopedMcpConfig;
|
|
65
|
+
reconnectAttempt?: number;
|
|
66
|
+
maxReconnectAttempts?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DisabledMCPServer {
|
|
70
|
+
name: string;
|
|
71
|
+
status: "disabled";
|
|
72
|
+
config: ScopedMcpConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DisconnectedMCPServer {
|
|
76
|
+
name: string;
|
|
77
|
+
status: "disconnected";
|
|
78
|
+
config: ScopedMcpConfig;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type MCPServerConnection =
|
|
82
|
+
| ConnectedMCPServer
|
|
83
|
+
| FailedMCPServer
|
|
84
|
+
| PendingMCPServer
|
|
85
|
+
| DisabledMCPServer
|
|
86
|
+
| DisconnectedMCPServer;
|
|
87
|
+
|
|
88
|
+
// Serialized Types
|
|
89
|
+
export interface SerializedTool {
|
|
90
|
+
name: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
inputSchema?: Tool["inputSchema"];
|
|
93
|
+
isMcp?: boolean;
|
|
94
|
+
serverName?: string;
|
|
95
|
+
originalToolName?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SerializedServer {
|
|
99
|
+
name: string;
|
|
100
|
+
status: ConnectionStatus;
|
|
101
|
+
capabilities?: ServerCapabilities;
|
|
102
|
+
serverInfo?: { name: string; version: string };
|
|
103
|
+
error?: string;
|
|
104
|
+
toolCount?: number;
|
|
105
|
+
resourceCount?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface MCPSerializedState {
|
|
109
|
+
servers: SerializedServer[];
|
|
110
|
+
configs: Record<string, ScopedMcpConfig>;
|
|
111
|
+
tools: SerializedTool[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Resource Types
|
|
115
|
+
export interface ServerResource extends Resource {
|
|
116
|
+
server: string;
|
|
117
|
+
serverName?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// MCP Tool Call Types
|
|
121
|
+
export interface McpToolCall {
|
|
122
|
+
serverName: string;
|
|
123
|
+
toolName: string;
|
|
124
|
+
arguments: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface McpToolResult {
|
|
128
|
+
success: boolean;
|
|
129
|
+
content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
130
|
+
error?: string;
|
|
131
|
+
isError?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Configuration Validation
|
|
135
|
+
export function isStdioConfig(config: McpServerConfig): boolean {
|
|
136
|
+
if (config.type === "stdio") return true;
|
|
137
|
+
return !!config.command && !config.url;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isRemoteConfig(config: McpServerConfig): boolean {
|
|
141
|
+
return !!config.url;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function validateMcpServerConfig(config: unknown): config is McpServerConfig {
|
|
145
|
+
if (!config || typeof config !== "object") return false;
|
|
146
|
+
const obj = config as Record<string, unknown>;
|
|
147
|
+
return ("command" in obj && typeof obj["command"] === "string") ||
|
|
148
|
+
("url" in obj && typeof obj["url"] === "string");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getServerTransport(config: McpServerConfig): Transport {
|
|
152
|
+
if (config.type === "stdio") return "stdio";
|
|
153
|
+
if (config.type === "sse" || config.type === "http" || config.type === "ws") return config.type;
|
|
154
|
+
return "stdio";
|
|
155
|
+
}
|