@opencoven/coven-code 0.0.1
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/README.md +145 -0
- package/bin/coven-code-sdk.mjs +12 -0
- package/bin/coven-code.mjs +19 -0
- package/docs/CLI.md +192 -0
- package/docs/CONFIGURATION.md +107 -0
- package/docs/DEVELOPMENT.md +104 -0
- package/docs/DOGFOOD-PROTOCOL.md +263 -0
- package/docs/MCP-SKILLS-PLUGINS.md +127 -0
- package/docs/README.md +38 -0
- package/docs/RELEASE.md +33 -0
- package/docs/SDK.md +107 -0
- package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +904 -0
- package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +670 -0
- package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +235 -0
- package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +63 -0
- package/package.json +36 -0
- package/src/agent/lane.mjs +136 -0
- package/src/agent/local.mjs +95 -0
- package/src/cli/dispatch.mjs +66 -0
- package/src/cli/execute.mjs +588 -0
- package/src/cli/help.mjs +58 -0
- package/src/cli/interactive-core.mjs +302 -0
- package/src/cli/notifications.mjs +13 -0
- package/src/cli/parse.mjs +83 -0
- package/src/cli/reasoning.mjs +45 -0
- package/src/cli/refs.mjs +162 -0
- package/src/cli/repl.mjs +61 -0
- package/src/cli/slash-commands.mjs +357 -0
- package/src/cli/stream-json.mjs +116 -0
- package/src/cli/tui.mjs +757 -0
- package/src/commands/agents.mjs +53 -0
- package/src/commands/config.mjs +27 -0
- package/src/commands/ide.mjs +17 -0
- package/src/commands/login.mjs +84 -0
- package/src/commands/mcp.mjs +176 -0
- package/src/commands/permissions.mjs +328 -0
- package/src/commands/plugins.mjs +86 -0
- package/src/commands/review.mjs +74 -0
- package/src/commands/skill.mjs +23 -0
- package/src/commands/threads.mjs +165 -0
- package/src/commands/tools.mjs +77 -0
- package/src/commands/update.mjs +31 -0
- package/src/commands/usage.mjs +34 -0
- package/src/constants.mjs +46 -0
- package/src/main.mjs +87 -0
- package/src/mcp/discover.mjs +154 -0
- package/src/mcp/permissions.mjs +52 -0
- package/src/mcp/probe.mjs +424 -0
- package/src/mcp/registry.mjs +96 -0
- package/src/plugins/discover.mjs +880 -0
- package/src/sdk-install.mjs +187 -0
- package/src/sdk.mjs +314 -0
- package/src/settings/load.mjs +134 -0
- package/src/settings/paths.mjs +101 -0
- package/src/skills/builtin/building-skills/SKILL.md +20 -0
- package/src/skills/discover.mjs +95 -0
- package/src/threads/store.mjs +176 -0
- package/src/tools/builtin/bash.mjs +110 -0
- package/src/tools/builtin/create-file.mjs +66 -0
- package/src/tools/builtin/edit-file.mjs +76 -0
- package/src/tools/builtin/finder.mjs +73 -0
- package/src/tools/builtin/glob.mjs +74 -0
- package/src/tools/builtin/grep.mjs +82 -0
- package/src/tools/builtin/index.mjs +83 -0
- package/src/tools/builtin/librarian.mjs +97 -0
- package/src/tools/builtin/look-at.mjs +92 -0
- package/src/tools/builtin/mcp.mjs +51 -0
- package/src/tools/builtin/mermaid.mjs +59 -0
- package/src/tools/builtin/oracle.mjs +56 -0
- package/src/tools/builtin/painter.mjs +81 -0
- package/src/tools/builtin/plugin-tool.mjs +53 -0
- package/src/tools/builtin/read-mcp-resource.mjs +63 -0
- package/src/tools/builtin/read-web-page.mjs +72 -0
- package/src/tools/builtin/read.mjs +59 -0
- package/src/tools/builtin/runtime.mjs +215 -0
- package/src/tools/builtin/task.mjs +63 -0
- package/src/tools/builtin/toolbox-tool.mjs +57 -0
- package/src/tools/builtin/undo-edit.mjs +97 -0
- package/src/tools/builtin/web-search.mjs +128 -0
- package/src/tools/toolbox.mjs +273 -0
- package/src/util/fs.mjs +13 -0
- package/src/util/glob.mjs +46 -0
- package/src/util/html.mjs +21 -0
- package/src/util/media.mjs +13 -0
- package/src/util/shell.mjs +24 -0
- package/src/util/table.mjs +11 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readManagedSettings, readSettings, readSettingsFile } from '../settings/load.mjs';
|
|
4
|
+
import { findWorkspaceSettingsFile } from '../settings/paths.mjs';
|
|
5
|
+
import {
|
|
6
|
+
isMcpServerAllowed,
|
|
7
|
+
mcpPermissionRules,
|
|
8
|
+
mcpPermissionStatus,
|
|
9
|
+
readWorkspaceMcpApprovals,
|
|
10
|
+
} from './permissions.mjs';
|
|
11
|
+
import { mcpRegistryGate, mcpRegistryStatus } from './registry.mjs';
|
|
12
|
+
import { listSkills } from '../skills/discover.mjs';
|
|
13
|
+
|
|
14
|
+
const MCP_SERVERS_SETTING = 'covenCode.mcpServers';
|
|
15
|
+
|
|
16
|
+
export function listConfiguredMcpServers(parsed = {}) {
|
|
17
|
+
const { userSettings, workspaceSettings, managedSettings, approvals, permissionRules, registryGate } = mcpSettings(parsed);
|
|
18
|
+
const byName = new Map();
|
|
19
|
+
|
|
20
|
+
for (const [name, config] of Object.entries(userSettings[MCP_SERVERS_SETTING] ?? {})) {
|
|
21
|
+
const expandedConfig = expandMcpServerConfigEnv(config);
|
|
22
|
+
byName.set(name, {
|
|
23
|
+
name,
|
|
24
|
+
config: expandedConfig,
|
|
25
|
+
source: 'user',
|
|
26
|
+
status: mcpServerStatus('approved', expandedConfig, permissionRules, registryGate),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
for (const [name, config] of Object.entries(workspaceSettings[MCP_SERVERS_SETTING] ?? {})) {
|
|
30
|
+
const expandedConfig = expandMcpServerConfigEnv(config);
|
|
31
|
+
byName.set(name, {
|
|
32
|
+
name,
|
|
33
|
+
config: expandedConfig,
|
|
34
|
+
source: 'workspace',
|
|
35
|
+
status: mcpServerStatus(approvals[name] ? 'approved' : 'awaiting approval', expandedConfig, permissionRules, registryGate),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
for (const [name, config] of Object.entries(managedSettings[MCP_SERVERS_SETTING] ?? {})) {
|
|
39
|
+
const expandedConfig = expandMcpServerConfigEnv(config);
|
|
40
|
+
byName.set(name, {
|
|
41
|
+
name,
|
|
42
|
+
config: expandedConfig,
|
|
43
|
+
source: 'managed',
|
|
44
|
+
status: mcpServerStatus('approved', expandedConfig, permissionRules, registryGate),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listActiveMcpServerEntries(parsed = {}, prompt = '') {
|
|
52
|
+
const { permissionRules, registryGate } = mcpSettings(parsed);
|
|
53
|
+
const inline = parseInlineMcpServers(parsed.mcpConfig)
|
|
54
|
+
.filter((server) => !isMcpServerDisabled(server.config))
|
|
55
|
+
.filter((server) => !mcpRegistryStatus(server.config, registryGate))
|
|
56
|
+
.filter((server) => isMcpServerAllowed(server.config, permissionRules))
|
|
57
|
+
.map((server) => ({
|
|
58
|
+
name: server.name,
|
|
59
|
+
status: 'connected',
|
|
60
|
+
source: 'cli',
|
|
61
|
+
config: server.config,
|
|
62
|
+
}));
|
|
63
|
+
const inlineNames = new Set(inline.map((server) => server.name));
|
|
64
|
+
const configured = listConfiguredMcpServers(parsed)
|
|
65
|
+
.filter((server) => server.status === 'approved')
|
|
66
|
+
.filter((server) => !inlineNames.has(server.name))
|
|
67
|
+
.map((server) => ({ ...server, status: 'connected' }));
|
|
68
|
+
const occupiedNames = new Set([...inline, ...configured].map((server) => server.name));
|
|
69
|
+
const skillServers = listSkillMcpServers(prompt, parsed)
|
|
70
|
+
.filter((server) => !isMcpServerDisabled(server.config))
|
|
71
|
+
.filter((server) => !mcpRegistryStatus(server.config, registryGate))
|
|
72
|
+
.filter((server) => isMcpServerAllowed(server.config, permissionRules))
|
|
73
|
+
.filter((server) => !occupiedNames.has(server.name))
|
|
74
|
+
.map((server) => ({ ...server, status: 'connected' }));
|
|
75
|
+
return [...inline, ...configured, ...skillServers];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mcpSettings(parsed = {}) {
|
|
79
|
+
const userSettings = readSettings(parsed);
|
|
80
|
+
const workspacePath = findWorkspaceSettingsFile(process.cwd());
|
|
81
|
+
const workspaceSettings = workspacePath ? readSettingsFile(workspacePath) : {};
|
|
82
|
+
const managedSettings = readManagedSettings();
|
|
83
|
+
return {
|
|
84
|
+
userSettings,
|
|
85
|
+
workspaceSettings,
|
|
86
|
+
managedSettings,
|
|
87
|
+
approvals: readWorkspaceMcpApprovals(process.cwd()),
|
|
88
|
+
permissionRules: mcpPermissionRules(userSettings, workspaceSettings, managedSettings),
|
|
89
|
+
registryGate: mcpRegistryGate(userSettings, workspaceSettings, managedSettings),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mcpServerStatus(defaultStatus, config, permissionRules, registryGate) {
|
|
94
|
+
if (isMcpServerDisabled(config)) return 'disabled';
|
|
95
|
+
const registryStatus = mcpRegistryStatus(config, registryGate);
|
|
96
|
+
if (registryStatus) return registryStatus;
|
|
97
|
+
return mcpPermissionStatus(defaultStatus, config, permissionRules);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isMcpServerDisabled(config = {}) {
|
|
101
|
+
return config.disabled === true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function listActiveMcpServers(parsed = {}) {
|
|
105
|
+
return listActiveMcpServerEntries(parsed).map(({ name, source }) => ({
|
|
106
|
+
name,
|
|
107
|
+
status: 'connected',
|
|
108
|
+
source,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function parseInlineMcpServers(raw) {
|
|
113
|
+
if (!raw) return [];
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(raw);
|
|
116
|
+
const servers = parsed.mcpServers ?? parsed[MCP_SERVERS_SETTING] ?? parsed;
|
|
117
|
+
return Object.keys(servers).map((name) => ({ name, config: expandMcpServerConfigEnv(servers[name]) }));
|
|
118
|
+
} catch {
|
|
119
|
+
return [{ name: 'inline', config: { command: expandEnvVars(raw) } }];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function expandMcpServerConfigEnv(value) {
|
|
124
|
+
if (typeof value === 'string') return expandEnvVars(value);
|
|
125
|
+
if (Array.isArray(value)) return value.map((entry) => expandMcpServerConfigEnv(entry));
|
|
126
|
+
if (value && typeof value === 'object') {
|
|
127
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, expandMcpServerConfigEnv(entry)]));
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function expandEnvVars(value) {
|
|
133
|
+
return String(value).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function formatMcpServerCommand(config) {
|
|
137
|
+
if (config.url) return config.url;
|
|
138
|
+
return [config.command, ...(config.args ?? [])].filter(Boolean).join(' ');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function listSkillMcpServers(prompt = '', parsed = {}) {
|
|
142
|
+
const lower = prompt.toLowerCase();
|
|
143
|
+
const servers = [];
|
|
144
|
+
for (const skill of listSkills({ parsed })) {
|
|
145
|
+
if (!lower.includes(skill.name.toLowerCase())) continue;
|
|
146
|
+
const mcpPath = path.join(skill.dir, 'mcp.json');
|
|
147
|
+
if (!existsSync(mcpPath)) continue;
|
|
148
|
+
const mcp = readSettingsFile(mcpPath);
|
|
149
|
+
for (const [name, config] of Object.entries(mcp)) {
|
|
150
|
+
servers.push({ name, config: expandMcpServerConfigEnv(config), source: `skill:${skill.name}` });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return servers;
|
|
154
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { findWorkspaceSettingsFile, workspaceSettingsFile } from '../settings/paths.mjs';
|
|
3
|
+
import { readManagedSettings, readSettings, readSettingsFile, writeSettingsFile } from '../settings/load.mjs';
|
|
4
|
+
import { globMatch } from '../util/glob.mjs';
|
|
5
|
+
|
|
6
|
+
const MCP_PERMISSIONS_SETTING = 'covenCode.mcpPermissions';
|
|
7
|
+
|
|
8
|
+
export function mcpPermissionRulesForCwd(parsed = {}) {
|
|
9
|
+
const userSettings = readSettings(parsed);
|
|
10
|
+
const workspacePath = findWorkspaceSettingsFile(process.cwd());
|
|
11
|
+
const workspaceSettings = workspacePath ? readSettingsFile(workspacePath) : {};
|
|
12
|
+
return mcpPermissionRules(userSettings, workspaceSettings, readManagedSettings());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mcpPermissionRules(userSettings = {}, workspaceSettings = {}, managedSettings = {}) {
|
|
16
|
+
if (Array.isArray(managedSettings[MCP_PERMISSIONS_SETTING])) return managedSettings[MCP_PERMISSIONS_SETTING];
|
|
17
|
+
if (Array.isArray(workspaceSettings[MCP_PERMISSIONS_SETTING])) return workspaceSettings[MCP_PERMISSIONS_SETTING];
|
|
18
|
+
if (Array.isArray(userSettings[MCP_PERMISSIONS_SETTING])) return userSettings[MCP_PERMISSIONS_SETTING];
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mcpPermissionStatus(defaultStatus, config, rules) {
|
|
23
|
+
return isMcpServerAllowed(config, rules) ? defaultStatus : 'rejected';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isMcpServerAllowed(config = {}, rules = []) {
|
|
27
|
+
for (const rule of rules) {
|
|
28
|
+
if (!rule || typeof rule !== 'object' || !rule.matches || typeof rule.matches !== 'object') continue;
|
|
29
|
+
if (!mcpPermissionRuleMatches(config, rule.matches)) continue;
|
|
30
|
+
return rule.action !== 'reject';
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mcpPermissionRuleMatches(config = {}, matches = {}) {
|
|
36
|
+
const entries = Object.entries(matches);
|
|
37
|
+
if (entries.length === 0) return false;
|
|
38
|
+
return entries.every(([field, pattern]) => {
|
|
39
|
+
if (field === 'command') return globMatch(pattern, config.command ?? '');
|
|
40
|
+
if (field === 'args') return globMatch(pattern, (config.args ?? []).join(' '));
|
|
41
|
+
if (field === 'url') return globMatch(pattern, config.url ?? '');
|
|
42
|
+
return false;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readWorkspaceMcpApprovals(cwd) {
|
|
47
|
+
return readSettingsFile(path.join(path.dirname(workspaceSettingsFile(cwd)), 'mcp-approvals.json'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function writeWorkspaceMcpApprovals(cwd, approvals) {
|
|
51
|
+
await writeSettingsFile(path.join(path.dirname(workspaceSettingsFile(cwd)), 'mcp-approvals.json'), approvals);
|
|
52
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { globMatch } from '../util/glob.mjs';
|
|
6
|
+
|
|
7
|
+
const remoteMcpSessions = new Map();
|
|
8
|
+
|
|
9
|
+
export async function discoverMcpToolRows(servers) {
|
|
10
|
+
const rows = [];
|
|
11
|
+
for (const server of servers) {
|
|
12
|
+
const toolDefs = filterIncludedMcpTools(
|
|
13
|
+
server.config,
|
|
14
|
+
server.source?.startsWith('skill:')
|
|
15
|
+
? skillMcpTools(server.config)
|
|
16
|
+
: await queryMcpTools(server.config, server.name),
|
|
17
|
+
);
|
|
18
|
+
for (const tool of toolDefs) {
|
|
19
|
+
rows.push([
|
|
20
|
+
`mcp__${server.name}__${tool.name}`,
|
|
21
|
+
'local-mcp',
|
|
22
|
+
tool.description || `Tool from ${server.name}`,
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return rows;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function mcpServerHealth(config = {}, serverName = '') {
|
|
30
|
+
if (config.url) {
|
|
31
|
+
const tools = await queryRemoteMcpTools(config, serverName);
|
|
32
|
+
return formatToolHealth(tools);
|
|
33
|
+
}
|
|
34
|
+
const result = queryLocalMcpToolsResult(config);
|
|
35
|
+
if (result.error) return `error ${result.error.code ?? result.error.message}`;
|
|
36
|
+
if ((result.status ?? 0) !== 0) return `error exit ${result.status}`;
|
|
37
|
+
return formatToolHealth(parseMcpToolsOutput(result.stdout));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatToolHealth(tools = []) {
|
|
41
|
+
return `ok ${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function filterIncludedMcpTools(config = {}, tools = []) {
|
|
45
|
+
return tools.filter((tool) => isMcpToolIncluded(config, tool.name));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isMcpToolIncluded(config = {}, toolName = '') {
|
|
49
|
+
if (!Array.isArray(config.includeTools) || config.includeTools.length === 0) return true;
|
|
50
|
+
return config.includeTools.some((pattern) => globMatch(pattern, toolName));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function queryMcpTools(config = {}, serverName = '') {
|
|
54
|
+
if (config.url) return queryRemoteMcpTools(config, serverName);
|
|
55
|
+
return queryLocalMcpTools(config);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function queryLocalMcpTools(config = {}) {
|
|
59
|
+
return parseMcpToolsOutput(queryLocalMcpToolsResult(config).stdout);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function queryLocalMcpToolsResult(config = {}) {
|
|
63
|
+
if (!config.command) return [];
|
|
64
|
+
return spawnSync(config.command, config.args ?? [], {
|
|
65
|
+
input: localMcpRequestInput('tools/list', {}),
|
|
66
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
timeout: 1500,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function callMcpTool(config = {}, name, args = {}, serverName = '') {
|
|
73
|
+
if (config.url) return callRemoteMcpTool(config, name, args, serverName);
|
|
74
|
+
return callLocalMcpTool(config, name, args);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readMcpResource(config = {}, uri, serverName = '') {
|
|
78
|
+
if (config.url) return readRemoteMcpResource(config, uri, serverName);
|
|
79
|
+
return readLocalMcpResource(config, uri);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function callLocalMcpTool(config = {}, name, args = {}) {
|
|
83
|
+
if (!config.command) return '';
|
|
84
|
+
const result = spawnSync(config.command, config.args ?? [], {
|
|
85
|
+
input: localMcpRequestInput('tools/call', { name, arguments: args }),
|
|
86
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
timeout: 1500,
|
|
89
|
+
});
|
|
90
|
+
return parseMcpCallOutput(result.stdout);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readLocalMcpResource(config = {}, uri) {
|
|
94
|
+
if (!config.command) return '';
|
|
95
|
+
const result = spawnSync(config.command, config.args ?? [], {
|
|
96
|
+
input: localMcpRequestInput('resources/read', { uri }),
|
|
97
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
timeout: 1500,
|
|
100
|
+
});
|
|
101
|
+
return parseMcpResourceOutput(result.stdout);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function localMcpRequestInput(method, params = {}) {
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
jsonrpc: '2.0',
|
|
108
|
+
id: 1,
|
|
109
|
+
method: 'initialize',
|
|
110
|
+
params: {
|
|
111
|
+
protocolVersion: '2025-06-18',
|
|
112
|
+
capabilities: {},
|
|
113
|
+
clientInfo: { name: 'coven-code', version: '0.0.0' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
117
|
+
{ jsonrpc: '2.0', id: 2, method, params },
|
|
118
|
+
].map((message) => JSON.stringify(message)).join('\n') + '\n';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function queryRemoteMcpTools(config = {}, serverName = '') {
|
|
122
|
+
const message = await postRemoteMcp(config, 'tools/list', {}, serverName);
|
|
123
|
+
if (Array.isArray(message.result?.tools)) return message.result.tools;
|
|
124
|
+
if (Array.isArray(message.tools)) return message.tools;
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function callRemoteMcpTool(config = {}, name, args = {}, serverName = '') {
|
|
129
|
+
const message = await postRemoteMcp(config, 'tools/call', { name, arguments: args }, serverName);
|
|
130
|
+
return parseMcpCallOutput(`${JSON.stringify(message)}\n`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readRemoteMcpResource(config = {}, uri, serverName = '') {
|
|
134
|
+
const message = await postRemoteMcp(config, 'resources/read', { uri }, serverName);
|
|
135
|
+
return parseMcpResourceOutput(`${JSON.stringify(message)}\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function postRemoteMcp(config = {}, method, params, serverName = '') {
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
141
|
+
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
|
|
142
|
+
try {
|
|
143
|
+
if (config.transport === 'sse') {
|
|
144
|
+
return await postLegacySseMcp(config, body, controller.signal, serverName);
|
|
145
|
+
}
|
|
146
|
+
const response = await fetch(config.url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
149
|
+
body,
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
});
|
|
152
|
+
rememberRemoteMcpSession(config, serverName, response);
|
|
153
|
+
if (response.status === 400 && !remoteMcpSessions.has(remoteMcpSessionKey(config, serverName)) && await initializeRemoteMcpSession(config, serverName, controller.signal)) {
|
|
154
|
+
const retry = await fetch(config.url, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
157
|
+
body,
|
|
158
|
+
signal: controller.signal,
|
|
159
|
+
});
|
|
160
|
+
rememberRemoteMcpSession(config, serverName, retry);
|
|
161
|
+
return parseRemoteMcpResponse(await retry.text());
|
|
162
|
+
}
|
|
163
|
+
if (response.status === 401 && await refreshMcpOauthToken(serverName, config)) {
|
|
164
|
+
const retry = await fetch(config.url, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
167
|
+
body,
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
});
|
|
170
|
+
rememberRemoteMcpSession(config, serverName, retry);
|
|
171
|
+
return parseRemoteMcpResponse(await retry.text());
|
|
172
|
+
}
|
|
173
|
+
if (response.status >= 400 && response.status < 500) {
|
|
174
|
+
return await postLegacySseMcp(config, body, controller.signal, serverName);
|
|
175
|
+
}
|
|
176
|
+
const text = await response.text();
|
|
177
|
+
return parseRemoteMcpResponse(text);
|
|
178
|
+
} catch {
|
|
179
|
+
return {};
|
|
180
|
+
} finally {
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function postLegacySseMcp(config = {}, body, signal, serverName = '') {
|
|
186
|
+
const endpoint = await discoverLegacySseEndpoint(config, signal, serverName);
|
|
187
|
+
if (!endpoint) return {};
|
|
188
|
+
const response = await fetch(endpoint, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
191
|
+
body,
|
|
192
|
+
signal,
|
|
193
|
+
});
|
|
194
|
+
rememberRemoteMcpSession(config, serverName, response);
|
|
195
|
+
if (response.status === 401 && await refreshMcpOauthToken(serverName, config)) {
|
|
196
|
+
const retry = await fetch(endpoint, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
199
|
+
body,
|
|
200
|
+
signal,
|
|
201
|
+
});
|
|
202
|
+
rememberRemoteMcpSession(config, serverName, retry);
|
|
203
|
+
return parseRemoteMcpResponse(await retry.text());
|
|
204
|
+
}
|
|
205
|
+
return parseRemoteMcpResponse(await response.text());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function discoverLegacySseEndpoint(config = {}, signal, serverName = '') {
|
|
209
|
+
const response = await fetch(config.url, {
|
|
210
|
+
method: 'GET',
|
|
211
|
+
headers: remoteMcpHeaders(config, 'text/event-stream', serverName),
|
|
212
|
+
signal,
|
|
213
|
+
});
|
|
214
|
+
if (!response.ok) return '';
|
|
215
|
+
return resolveRemoteMcpUrl(config.url, parseLegacySseEndpoint(await response.text()));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function remoteMcpHeaders(config = {}, accept, serverName = '') {
|
|
219
|
+
return {
|
|
220
|
+
'content-type': 'application/json',
|
|
221
|
+
accept,
|
|
222
|
+
...oauthMcpHeaders(serverName),
|
|
223
|
+
...remoteMcpSessionHeader(config, serverName),
|
|
224
|
+
...(config.headers ?? {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function initializeRemoteMcpSession(config = {}, serverName = '', signal) {
|
|
229
|
+
const response = await fetch(config.url, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
jsonrpc: '2.0',
|
|
234
|
+
id: 0,
|
|
235
|
+
method: 'initialize',
|
|
236
|
+
params: {
|
|
237
|
+
protocolVersion: '2025-06-18',
|
|
238
|
+
capabilities: {},
|
|
239
|
+
clientInfo: { name: 'coven-code', version: '0.0.0' },
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
signal,
|
|
243
|
+
});
|
|
244
|
+
rememberRemoteMcpSession(config, serverName, response);
|
|
245
|
+
if (!response.ok || !remoteMcpSessions.has(remoteMcpSessionKey(config, serverName))) return false;
|
|
246
|
+
await fetch(config.url, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
|
|
249
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
250
|
+
signal,
|
|
251
|
+
});
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function rememberRemoteMcpSession(config = {}, serverName = '', response) {
|
|
256
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
257
|
+
if (sessionId) remoteMcpSessions.set(remoteMcpSessionKey(config, serverName), sessionId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function remoteMcpSessionHeader(config = {}, serverName = '') {
|
|
261
|
+
const sessionId = remoteMcpSessions.get(remoteMcpSessionKey(config, serverName));
|
|
262
|
+
return sessionId ? { 'Mcp-Session-Id': sessionId } : {};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function remoteMcpSessionKey(config = {}, serverName = '') {
|
|
266
|
+
return `${serverName}\n${config.url ?? ''}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function oauthMcpHeaders(serverName = '') {
|
|
270
|
+
const credential = readMcpOauthCredential(serverName);
|
|
271
|
+
return credential.accessToken || credential.access_token ? { Authorization: `Bearer ${credential.accessToken ?? credential.access_token}` } : {};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readMcpOauthCredential(serverName = '') {
|
|
275
|
+
if (!serverName) return {};
|
|
276
|
+
try {
|
|
277
|
+
return JSON.parse(readFileSync(mcpOauthCredentialPath(serverName), 'utf8'));
|
|
278
|
+
} catch {
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function refreshMcpOauthToken(serverName = '', config = {}) {
|
|
284
|
+
if (hasExplicitAuthorizationHeader(config)) return false;
|
|
285
|
+
const credential = readMcpOauthCredential(serverName);
|
|
286
|
+
const refreshToken = credential.refreshToken ?? credential.refresh_token;
|
|
287
|
+
const tokenUrl = credential.tokenUrl ?? credential.token_url;
|
|
288
|
+
if (!refreshToken || !tokenUrl) return false;
|
|
289
|
+
const response = await fetch(tokenUrl, {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
292
|
+
body: new URLSearchParams({
|
|
293
|
+
grant_type: 'refresh_token',
|
|
294
|
+
refresh_token: refreshToken,
|
|
295
|
+
...(credential.clientId || credential.client_id ? { client_id: credential.clientId ?? credential.client_id } : {}),
|
|
296
|
+
...(credential.clientSecret || credential.client_secret ? { client_secret: credential.clientSecret ?? credential.client_secret } : {}),
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) return false;
|
|
300
|
+
const token = await response.json();
|
|
301
|
+
const nextCredential = {
|
|
302
|
+
...credential,
|
|
303
|
+
accessToken: token.access_token ?? token.accessToken ?? credential.accessToken,
|
|
304
|
+
refreshToken: token.refresh_token ?? token.refreshToken ?? credential.refreshToken,
|
|
305
|
+
...(token.expires_in || token.expiresIn ? { expiresAt: Date.now() + Number(token.expires_in ?? token.expiresIn) * 1000 } : {}),
|
|
306
|
+
};
|
|
307
|
+
writeFileSync(mcpOauthCredentialPath(serverName), `${JSON.stringify(nextCredential, null, 2)}\n`);
|
|
308
|
+
return Boolean(nextCredential.accessToken);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function hasExplicitAuthorizationHeader(config = {}) {
|
|
312
|
+
return Object.keys(config.headers ?? {}).some((key) => key.toLowerCase() === 'authorization');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function mcpOauthCredentialPath(serverName) {
|
|
316
|
+
return path.join(os.homedir(), '.coven-code', 'oauth', `${serverName}.json`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseLegacySseEndpoint(text = '') {
|
|
320
|
+
let event = 'message';
|
|
321
|
+
const data = [];
|
|
322
|
+
for (const line of text.split(/\r?\n/)) {
|
|
323
|
+
if (!line) {
|
|
324
|
+
if (event === 'endpoint' && data.length) return data.join('\n').trim();
|
|
325
|
+
event = 'message';
|
|
326
|
+
data.length = 0;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (line.startsWith(':')) continue;
|
|
330
|
+
const separator = line.indexOf(':');
|
|
331
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
332
|
+
const value = separator === -1 ? '' : line.slice(separator + 1).replace(/^ /, '');
|
|
333
|
+
if (field === 'event') event = value;
|
|
334
|
+
if (field === 'data') data.push(value);
|
|
335
|
+
}
|
|
336
|
+
if (event === 'endpoint' && data.length) return data.join('\n').trim();
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function resolveRemoteMcpUrl(base, endpoint) {
|
|
341
|
+
if (!endpoint) return '';
|
|
342
|
+
try {
|
|
343
|
+
return new URL(endpoint, base).href;
|
|
344
|
+
} catch {
|
|
345
|
+
return '';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function parseRemoteMcpResponse(text = '') {
|
|
350
|
+
for (const chunk of text.split(/\r?\n/).filter(Boolean)) {
|
|
351
|
+
const line = chunk.startsWith('data:') ? chunk.slice('data:'.length).trim() : chunk.trim();
|
|
352
|
+
if (!line || line === '[DONE]') continue;
|
|
353
|
+
try {
|
|
354
|
+
return JSON.parse(line);
|
|
355
|
+
} catch {
|
|
356
|
+
// Remote MCP servers can include diagnostic or event wrapper lines.
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
return JSON.parse(text);
|
|
361
|
+
} catch {
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseMcpCallOutput(stdout = '') {
|
|
367
|
+
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
368
|
+
try {
|
|
369
|
+
const message = JSON.parse(line);
|
|
370
|
+
const content = message.result?.content ?? message.content;
|
|
371
|
+
if (Array.isArray(content)) {
|
|
372
|
+
return content.map((entry) => entry.text ?? entry.content ?? JSON.stringify(entry)).join('\n');
|
|
373
|
+
}
|
|
374
|
+
if (typeof content === 'string') return content;
|
|
375
|
+
if (message.result !== undefined) return JSON.stringify(message.result);
|
|
376
|
+
} catch {
|
|
377
|
+
// Non-JSON diagnostic output from MCP server startup is ignored.
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return '';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseMcpResourceOutput(stdout = '') {
|
|
384
|
+
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
385
|
+
try {
|
|
386
|
+
const message = JSON.parse(line);
|
|
387
|
+
const contents = message.result?.contents ?? message.contents;
|
|
388
|
+
if (Array.isArray(contents)) {
|
|
389
|
+
return contents.map((entry) => entry.text ?? entry.content ?? entry.blob ?? JSON.stringify(entry)).join('\n');
|
|
390
|
+
}
|
|
391
|
+
if (typeof contents === 'string') return contents;
|
|
392
|
+
if (message.result !== undefined) return JSON.stringify(message.result);
|
|
393
|
+
} catch {
|
|
394
|
+
// Non-JSON diagnostic output from MCP server startup is ignored.
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return '';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseMcpToolsOutput(stdout = '') {
|
|
401
|
+
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
402
|
+
try {
|
|
403
|
+
const message = JSON.parse(line);
|
|
404
|
+
if (Array.isArray(message.result?.tools)) return message.result.tools;
|
|
405
|
+
if (Array.isArray(message.tools)) return message.tools;
|
|
406
|
+
} catch {
|
|
407
|
+
// Non-JSON diagnostic output from MCP server startup is ignored.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function skillMcpTools(config = {}) {
|
|
414
|
+
return (config.includeTools ?? []).map((name) => ({
|
|
415
|
+
name,
|
|
416
|
+
description: `Tool from skill MCP server ${config.command || config.url || ''}`.trim(),
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function parseMcpToolName(name) {
|
|
421
|
+
const match = name.match(/^mcp__([^_]+(?:_[^_]+)*)__([\s\S]+)$/);
|
|
422
|
+
if (!match) return undefined;
|
|
423
|
+
return { serverName: match[1], toolName: match[2] };
|
|
424
|
+
}
|