@link-assistant/agent 0.0.9 → 0.0.12
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/EXAMPLES.md +36 -0
- package/MODELS.md +72 -24
- package/README.md +59 -2
- package/TOOLS.md +20 -0
- package/package.json +35 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +469 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +144 -119
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +39 -24
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +346 -199
- package/src/json-standard/index.ts +67 -51
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
|
@@ -6,65 +6,76 @@
|
|
|
6
6
|
* - claude: Claude CLI stream-json format - NDJSON (newline-delimited JSON)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { EOL } from 'os'
|
|
9
|
+
import { EOL } from 'os';
|
|
10
10
|
|
|
11
|
-
export type JsonStandard = 'opencode' | 'claude'
|
|
11
|
+
export type JsonStandard = 'opencode' | 'claude';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* OpenCode JSON event types
|
|
15
15
|
*/
|
|
16
16
|
export interface OpenCodeEvent {
|
|
17
|
-
type: 'step_start' | 'step_finish' | 'text' | 'tool_use' | 'error'
|
|
18
|
-
timestamp: number
|
|
19
|
-
sessionID: string
|
|
20
|
-
part?: Record<string, unknown
|
|
21
|
-
error?: string | Record<string, unknown
|
|
17
|
+
type: 'step_start' | 'step_finish' | 'text' | 'tool_use' | 'error';
|
|
18
|
+
timestamp: number;
|
|
19
|
+
sessionID: string;
|
|
20
|
+
part?: Record<string, unknown>;
|
|
21
|
+
error?: string | Record<string, unknown>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Claude JSON event types (stream-json format)
|
|
26
26
|
*/
|
|
27
27
|
export interface ClaudeEvent {
|
|
28
|
-
type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'result'
|
|
29
|
-
timestamp?: string
|
|
30
|
-
session_id?: string
|
|
31
|
-
role?: 'assistant' | 'user'
|
|
32
|
-
content?: Array<{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'result';
|
|
29
|
+
timestamp?: string;
|
|
30
|
+
session_id?: string;
|
|
31
|
+
role?: 'assistant' | 'user';
|
|
32
|
+
content?: Array<{
|
|
33
|
+
type: 'text' | 'tool_use';
|
|
34
|
+
text?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
input?: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
output?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
input?: unknown;
|
|
41
|
+
tool_use_id?: string;
|
|
42
|
+
status?: 'success' | 'error';
|
|
43
|
+
duration_ms?: number;
|
|
44
|
+
model?: string;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* Serialize JSON output based on the selected standard
|
|
44
49
|
*/
|
|
45
|
-
export function serializeOutput(
|
|
50
|
+
export function serializeOutput(
|
|
51
|
+
event: OpenCodeEvent | ClaudeEvent,
|
|
52
|
+
standard: JsonStandard
|
|
53
|
+
): string {
|
|
46
54
|
if (standard === 'claude') {
|
|
47
55
|
// NDJSON format - compact, one line
|
|
48
|
-
return JSON.stringify(event) + EOL
|
|
56
|
+
return JSON.stringify(event) + EOL;
|
|
49
57
|
}
|
|
50
58
|
// OpenCode format - pretty-printed
|
|
51
|
-
return JSON.stringify(event, null, 2) + EOL
|
|
59
|
+
return JSON.stringify(event, null, 2) + EOL;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
55
63
|
* Convert OpenCode event to Claude event format
|
|
56
64
|
*/
|
|
57
|
-
export function convertOpenCodeToClaude(
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
export function convertOpenCodeToClaude(
|
|
66
|
+
event: OpenCodeEvent,
|
|
67
|
+
startTime: number
|
|
68
|
+
): ClaudeEvent | null {
|
|
69
|
+
const timestamp = new Date(event.timestamp).toISOString();
|
|
70
|
+
const session_id = event.sessionID;
|
|
60
71
|
|
|
61
72
|
switch (event.type) {
|
|
62
73
|
case 'step_start':
|
|
63
74
|
return {
|
|
64
75
|
type: 'init',
|
|
65
76
|
timestamp,
|
|
66
|
-
session_id
|
|
67
|
-
}
|
|
77
|
+
session_id,
|
|
78
|
+
};
|
|
68
79
|
|
|
69
80
|
case 'text':
|
|
70
81
|
if (event.part && typeof event.part.text === 'string') {
|
|
@@ -73,28 +84,30 @@ export function convertOpenCodeToClaude(event: OpenCodeEvent, startTime: number)
|
|
|
73
84
|
timestamp,
|
|
74
85
|
session_id,
|
|
75
86
|
role: 'assistant',
|
|
76
|
-
content: [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: event.part.text,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
81
94
|
}
|
|
82
|
-
return null
|
|
95
|
+
return null;
|
|
83
96
|
|
|
84
97
|
case 'tool_use':
|
|
85
98
|
if (event.part && event.part.state) {
|
|
86
|
-
const state = event.part.state as Record<string, unknown
|
|
87
|
-
const tool = state.tool as Record<string, unknown> | undefined
|
|
99
|
+
const state = event.part.state as Record<string, unknown>;
|
|
100
|
+
const tool = state.tool as Record<string, unknown> | undefined;
|
|
88
101
|
return {
|
|
89
102
|
type: 'tool_use',
|
|
90
103
|
timestamp,
|
|
91
104
|
session_id,
|
|
92
105
|
name: (tool?.name as string) || 'unknown',
|
|
93
106
|
input: tool?.parameters || {},
|
|
94
|
-
tool_use_id: event.part.id as string
|
|
95
|
-
}
|
|
107
|
+
tool_use_id: event.part.id as string,
|
|
108
|
+
};
|
|
96
109
|
}
|
|
97
|
-
return null
|
|
110
|
+
return null;
|
|
98
111
|
|
|
99
112
|
case 'step_finish':
|
|
100
113
|
return {
|
|
@@ -102,8 +115,8 @@ export function convertOpenCodeToClaude(event: OpenCodeEvent, startTime: number)
|
|
|
102
115
|
timestamp,
|
|
103
116
|
session_id,
|
|
104
117
|
status: 'success',
|
|
105
|
-
duration_ms: event.timestamp - startTime
|
|
106
|
-
}
|
|
118
|
+
duration_ms: event.timestamp - startTime,
|
|
119
|
+
};
|
|
107
120
|
|
|
108
121
|
case 'error':
|
|
109
122
|
return {
|
|
@@ -111,11 +124,14 @@ export function convertOpenCodeToClaude(event: OpenCodeEvent, startTime: number)
|
|
|
111
124
|
timestamp,
|
|
112
125
|
session_id,
|
|
113
126
|
status: 'error',
|
|
114
|
-
output:
|
|
115
|
-
|
|
127
|
+
output:
|
|
128
|
+
typeof event.error === 'string'
|
|
129
|
+
? event.error
|
|
130
|
+
: JSON.stringify(event.error),
|
|
131
|
+
};
|
|
116
132
|
|
|
117
133
|
default:
|
|
118
|
-
return null
|
|
134
|
+
return null;
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
|
|
@@ -123,7 +139,7 @@ export function convertOpenCodeToClaude(event: OpenCodeEvent, startTime: number)
|
|
|
123
139
|
* Create an event output handler based on the selected standard
|
|
124
140
|
*/
|
|
125
141
|
export function createEventHandler(standard: JsonStandard, sessionID: string) {
|
|
126
|
-
const startTime = Date.now()
|
|
142
|
+
const startTime = Date.now();
|
|
127
143
|
|
|
128
144
|
return {
|
|
129
145
|
/**
|
|
@@ -131,12 +147,12 @@ export function createEventHandler(standard: JsonStandard, sessionID: string) {
|
|
|
131
147
|
*/
|
|
132
148
|
output(event: OpenCodeEvent): void {
|
|
133
149
|
if (standard === 'claude') {
|
|
134
|
-
const claudeEvent = convertOpenCodeToClaude(event, startTime)
|
|
150
|
+
const claudeEvent = convertOpenCodeToClaude(event, startTime);
|
|
135
151
|
if (claudeEvent) {
|
|
136
|
-
process.stdout.write(serializeOutput(claudeEvent, standard))
|
|
152
|
+
process.stdout.write(serializeOutput(claudeEvent, standard));
|
|
137
153
|
}
|
|
138
154
|
} else {
|
|
139
|
-
process.stdout.write(serializeOutput(event, standard))
|
|
155
|
+
process.stdout.write(serializeOutput(event, standard));
|
|
140
156
|
}
|
|
141
157
|
},
|
|
142
158
|
|
|
@@ -144,14 +160,14 @@ export function createEventHandler(standard: JsonStandard, sessionID: string) {
|
|
|
144
160
|
* Get the start time for duration calculations
|
|
145
161
|
*/
|
|
146
162
|
getStartTime(): number {
|
|
147
|
-
return startTime
|
|
148
|
-
}
|
|
149
|
-
}
|
|
163
|
+
return startTime;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
/**
|
|
153
169
|
* Validate JSON standard option
|
|
154
170
|
*/
|
|
155
171
|
export function isValidJsonStandard(value: string): value is JsonStandard {
|
|
156
|
-
return value === 'opencode' || value === 'claude'
|
|
172
|
+
return value === 'opencode' || value === 'claude';
|
|
157
173
|
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
import { experimental_createMCPClient } from
|
|
2
|
-
import { type Tool } from
|
|
3
|
-
import { StreamableHTTPClientTransport } from
|
|
4
|
-
import { SSEClientTransport } from
|
|
5
|
-
import { StdioClientTransport } from
|
|
6
|
-
import { Config } from
|
|
7
|
-
import { Log } from
|
|
8
|
-
import { NamedError } from
|
|
9
|
-
import z from
|
|
10
|
-
import { Instance } from
|
|
11
|
-
import { withTimeout } from
|
|
1
|
+
import { experimental_createMCPClient } from '@ai-sdk/mcp';
|
|
2
|
+
import { type Tool } from 'ai';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { Config } from '../config/config';
|
|
7
|
+
import { Log } from '../util/log';
|
|
8
|
+
import { NamedError } from '../util/error';
|
|
9
|
+
import z from 'zod/v4';
|
|
10
|
+
import { Instance } from '../project/instance';
|
|
11
|
+
import { withTimeout } from '../util/timeout';
|
|
12
12
|
|
|
13
13
|
export namespace MCP {
|
|
14
|
-
const log = Log.create({ service:
|
|
14
|
+
const log = Log.create({ service: 'mcp' });
|
|
15
15
|
|
|
16
16
|
export const Failed = NamedError.create(
|
|
17
|
-
|
|
17
|
+
'MCPFailed',
|
|
18
18
|
z.object({
|
|
19
19
|
name: z.string(),
|
|
20
|
-
})
|
|
21
|
-
)
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
22
|
|
|
23
|
-
type Client = Awaited<ReturnType<typeof experimental_createMCPClient
|
|
23
|
+
type Client = Awaited<ReturnType<typeof experimental_createMCPClient>>;
|
|
24
24
|
|
|
25
25
|
export const Status = z
|
|
26
|
-
.discriminatedUnion(
|
|
26
|
+
.discriminatedUnion('status', [
|
|
27
27
|
z
|
|
28
28
|
.object({
|
|
29
|
-
status: z.literal(
|
|
29
|
+
status: z.literal('connected'),
|
|
30
30
|
})
|
|
31
31
|
.meta({
|
|
32
|
-
ref:
|
|
32
|
+
ref: 'MCPStatusConnected',
|
|
33
33
|
}),
|
|
34
34
|
z
|
|
35
35
|
.object({
|
|
36
|
-
status: z.literal(
|
|
36
|
+
status: z.literal('disabled'),
|
|
37
37
|
})
|
|
38
38
|
.meta({
|
|
39
|
-
ref:
|
|
39
|
+
ref: 'MCPStatusDisabled',
|
|
40
40
|
}),
|
|
41
41
|
z
|
|
42
42
|
.object({
|
|
43
|
-
status: z.literal(
|
|
43
|
+
status: z.literal('failed'),
|
|
44
44
|
error: z.string(),
|
|
45
45
|
})
|
|
46
46
|
.meta({
|
|
47
|
-
ref:
|
|
47
|
+
ref: 'MCPStatusFailed',
|
|
48
48
|
}),
|
|
49
49
|
])
|
|
50
50
|
.meta({
|
|
51
|
-
ref:
|
|
52
|
-
})
|
|
53
|
-
export type Status = z.infer<typeof Status
|
|
54
|
-
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient
|
|
51
|
+
ref: 'MCPStatus',
|
|
52
|
+
});
|
|
53
|
+
export type Status = z.infer<typeof Status>;
|
|
54
|
+
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;
|
|
55
55
|
|
|
56
56
|
const state = Instance.state(
|
|
57
57
|
async () => {
|
|
58
|
-
const cfg = await Config.get()
|
|
59
|
-
const config = cfg.mcp ?? {}
|
|
60
|
-
const clients: Record<string, Client> = {}
|
|
61
|
-
const status: Record<string, Status> = {}
|
|
58
|
+
const cfg = await Config.get();
|
|
59
|
+
const config = cfg.mcp ?? {};
|
|
60
|
+
const clients: Record<string, Client> = {};
|
|
61
|
+
const status: Record<string, Status> = {};
|
|
62
62
|
|
|
63
63
|
await Promise.all(
|
|
64
64
|
Object.entries(config).map(async ([key, mcp]) => {
|
|
65
|
-
const result = await create(key, mcp).catch(() => undefined)
|
|
66
|
-
if (!result) return
|
|
65
|
+
const result = await create(key, mcp).catch(() => undefined);
|
|
66
|
+
if (!result) return;
|
|
67
67
|
|
|
68
|
-
status[key] = result.status
|
|
68
|
+
status[key] = result.status;
|
|
69
69
|
|
|
70
70
|
if (result.mcpClient) {
|
|
71
|
-
clients[key] = result.mcpClient
|
|
71
|
+
clients[key] = result.mcpClient;
|
|
72
72
|
}
|
|
73
|
-
})
|
|
74
|
-
)
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
75
|
return {
|
|
76
76
|
status,
|
|
77
77
|
clients,
|
|
78
|
-
}
|
|
78
|
+
};
|
|
79
79
|
},
|
|
80
80
|
async (state) => {
|
|
81
81
|
await Promise.all(
|
|
82
82
|
Object.values(state.clients).map((client) =>
|
|
83
83
|
client.close().catch((error) => {
|
|
84
|
-
log.error(
|
|
84
|
+
log.error('Failed to close MCP client', {
|
|
85
85
|
error,
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
)
|
|
86
|
+
});
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
92
|
|
|
93
93
|
export async function add(name: string, mcp: Config.Mcp) {
|
|
94
|
-
const s = await state()
|
|
95
|
-
const result = await create(name, mcp)
|
|
94
|
+
const s = await state();
|
|
95
|
+
const result = await create(name, mcp);
|
|
96
96
|
if (!result) {
|
|
97
97
|
const status = {
|
|
98
|
-
status:
|
|
99
|
-
error:
|
|
100
|
-
}
|
|
101
|
-
s.status[name] = status
|
|
98
|
+
status: 'failed' as const,
|
|
99
|
+
error: 'unknown error',
|
|
100
|
+
};
|
|
101
|
+
s.status[name] = status;
|
|
102
102
|
return {
|
|
103
103
|
status,
|
|
104
|
-
}
|
|
104
|
+
};
|
|
105
105
|
}
|
|
106
106
|
if (!result.mcpClient) {
|
|
107
|
-
s.status[name] = result.status
|
|
107
|
+
s.status[name] = result.status;
|
|
108
108
|
return {
|
|
109
109
|
status: s.status,
|
|
110
|
-
}
|
|
110
|
+
};
|
|
111
111
|
}
|
|
112
|
-
s.clients[name] = result.mcpClient
|
|
113
|
-
s.status[name] = result.status
|
|
112
|
+
s.clients[name] = result.mcpClient;
|
|
113
|
+
s.status[name] = result.status;
|
|
114
114
|
|
|
115
115
|
return {
|
|
116
116
|
status: s.status,
|
|
117
|
-
}
|
|
117
|
+
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
async function create(key: string, mcp: Config.Mcp) {
|
|
121
121
|
if (mcp.enabled === false) {
|
|
122
|
-
log.info(
|
|
123
|
-
return
|
|
122
|
+
log.info('mcp server disabled', { key });
|
|
123
|
+
return;
|
|
124
124
|
}
|
|
125
|
-
log.info(
|
|
126
|
-
let mcpClient: MCPClient | undefined
|
|
127
|
-
let status: Status | undefined = undefined
|
|
125
|
+
log.info('found', { key, type: mcp.type });
|
|
126
|
+
let mcpClient: MCPClient | undefined;
|
|
127
|
+
let status: Status | undefined = undefined;
|
|
128
128
|
|
|
129
|
-
if (mcp.type ===
|
|
129
|
+
if (mcp.type === 'remote') {
|
|
130
130
|
const transports = [
|
|
131
131
|
{
|
|
132
|
-
name:
|
|
132
|
+
name: 'StreamableHTTP',
|
|
133
133
|
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
|
|
134
134
|
requestInit: {
|
|
135
135
|
headers: mcp.headers,
|
|
@@ -137,153 +137,160 @@ export namespace MCP {
|
|
|
137
137
|
}),
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
|
-
name:
|
|
140
|
+
name: 'SSE',
|
|
141
141
|
transport: new SSEClientTransport(new URL(mcp.url), {
|
|
142
142
|
requestInit: {
|
|
143
143
|
headers: mcp.headers,
|
|
144
144
|
},
|
|
145
145
|
}),
|
|
146
146
|
},
|
|
147
|
-
]
|
|
148
|
-
let lastError: Error | undefined
|
|
147
|
+
];
|
|
148
|
+
let lastError: Error | undefined;
|
|
149
149
|
for (const { name, transport } of transports) {
|
|
150
150
|
const result = await experimental_createMCPClient({
|
|
151
|
-
name:
|
|
151
|
+
name: 'opencode',
|
|
152
152
|
transport,
|
|
153
153
|
})
|
|
154
154
|
.then((client) => {
|
|
155
|
-
log.info(
|
|
156
|
-
mcpClient = client
|
|
157
|
-
status = { status:
|
|
158
|
-
return true
|
|
155
|
+
log.info('connected', { key, transport: name });
|
|
156
|
+
mcpClient = client;
|
|
157
|
+
status = { status: 'connected' };
|
|
158
|
+
return true;
|
|
159
159
|
})
|
|
160
160
|
.catch((error) => {
|
|
161
|
-
lastError =
|
|
162
|
-
|
|
161
|
+
lastError =
|
|
162
|
+
error instanceof Error ? error : new Error(String(error));
|
|
163
|
+
log.debug('transport connection failed', {
|
|
163
164
|
key,
|
|
164
165
|
transport: name,
|
|
165
166
|
url: mcp.url,
|
|
166
167
|
error: lastError.message,
|
|
167
|
-
})
|
|
168
|
+
});
|
|
168
169
|
status = {
|
|
169
|
-
status:
|
|
170
|
+
status: 'failed' as const,
|
|
170
171
|
error: lastError.message,
|
|
171
|
-
}
|
|
172
|
-
return false
|
|
173
|
-
})
|
|
174
|
-
if (result) break
|
|
172
|
+
};
|
|
173
|
+
return false;
|
|
174
|
+
});
|
|
175
|
+
if (result) break;
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
if (mcp.type ===
|
|
179
|
-
const [cmd, ...args] = mcp.command
|
|
179
|
+
if (mcp.type === 'local') {
|
|
180
|
+
const [cmd, ...args] = mcp.command;
|
|
180
181
|
await experimental_createMCPClient({
|
|
181
|
-
name:
|
|
182
|
+
name: 'opencode',
|
|
182
183
|
transport: new StdioClientTransport({
|
|
183
|
-
stderr:
|
|
184
|
+
stderr: 'ignore',
|
|
184
185
|
command: cmd,
|
|
185
186
|
args,
|
|
186
187
|
env: {
|
|
187
188
|
...process.env,
|
|
188
|
-
...(cmd ===
|
|
189
|
+
...(cmd === 'opencode' ? { BUN_BE_BUN: '1' } : {}),
|
|
189
190
|
...mcp.environment,
|
|
190
191
|
},
|
|
191
192
|
}),
|
|
192
193
|
})
|
|
193
194
|
.then((client) => {
|
|
194
|
-
mcpClient = client
|
|
195
|
+
mcpClient = client;
|
|
195
196
|
status = {
|
|
196
|
-
status:
|
|
197
|
-
}
|
|
197
|
+
status: 'connected',
|
|
198
|
+
};
|
|
198
199
|
})
|
|
199
200
|
.catch((error) => {
|
|
200
|
-
log.error(
|
|
201
|
+
log.error('local mcp startup failed', {
|
|
201
202
|
key,
|
|
202
203
|
command: mcp.command,
|
|
203
204
|
error: error instanceof Error ? error.message : String(error),
|
|
204
|
-
})
|
|
205
|
+
});
|
|
205
206
|
status = {
|
|
206
|
-
status:
|
|
207
|
+
status: 'failed' as const,
|
|
207
208
|
error: error instanceof Error ? error.message : String(error),
|
|
208
|
-
}
|
|
209
|
-
})
|
|
209
|
+
};
|
|
210
|
+
});
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
if (!status) {
|
|
213
214
|
status = {
|
|
214
|
-
status:
|
|
215
|
-
error:
|
|
216
|
-
}
|
|
215
|
+
status: 'failed' as const,
|
|
216
|
+
error: 'Unknown error',
|
|
217
|
+
};
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
if (!mcpClient) {
|
|
220
221
|
return {
|
|
221
222
|
mcpClient: undefined,
|
|
222
223
|
status,
|
|
223
|
-
}
|
|
224
|
+
};
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
const result = await withTimeout(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
const result = await withTimeout(
|
|
228
|
+
mcpClient.tools(),
|
|
229
|
+
mcp.timeout ?? 5000
|
|
230
|
+
).catch((err) => {
|
|
231
|
+
log.error('failed to get tools from client', { key, error: err });
|
|
232
|
+
return undefined;
|
|
233
|
+
});
|
|
230
234
|
if (!result) {
|
|
231
235
|
await mcpClient.close().catch((error) => {
|
|
232
|
-
log.error(
|
|
236
|
+
log.error('Failed to close MCP client', {
|
|
233
237
|
error,
|
|
234
|
-
})
|
|
235
|
-
})
|
|
238
|
+
});
|
|
239
|
+
});
|
|
236
240
|
status = {
|
|
237
|
-
status:
|
|
238
|
-
error:
|
|
239
|
-
}
|
|
241
|
+
status: 'failed',
|
|
242
|
+
error: 'Failed to get tools',
|
|
243
|
+
};
|
|
240
244
|
return {
|
|
241
245
|
mcpClient: undefined,
|
|
242
246
|
status: {
|
|
243
|
-
status:
|
|
244
|
-
error:
|
|
247
|
+
status: 'failed' as const,
|
|
248
|
+
error: 'Failed to get tools',
|
|
245
249
|
},
|
|
246
|
-
}
|
|
250
|
+
};
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
log.info(
|
|
253
|
+
log.info('create() successfully created client', {
|
|
254
|
+
key,
|
|
255
|
+
toolCount: Object.keys(result).length,
|
|
256
|
+
});
|
|
250
257
|
return {
|
|
251
258
|
mcpClient,
|
|
252
259
|
status,
|
|
253
|
-
}
|
|
260
|
+
};
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
export async function status() {
|
|
257
|
-
return state().then((state) => state.status)
|
|
264
|
+
return state().then((state) => state.status);
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
export async function clients() {
|
|
261
|
-
return state().then((state) => state.clients)
|
|
268
|
+
return state().then((state) => state.clients);
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
export async function tools() {
|
|
265
|
-
const result: Record<string, Tool> = {}
|
|
266
|
-
const s = await state()
|
|
267
|
-
const clientsSnapshot = await clients()
|
|
272
|
+
const result: Record<string, Tool> = {};
|
|
273
|
+
const s = await state();
|
|
274
|
+
const clientsSnapshot = await clients();
|
|
268
275
|
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
|
|
269
276
|
const tools = await client.tools().catch((e) => {
|
|
270
|
-
log.error(
|
|
277
|
+
log.error('failed to get tools', { clientName, error: e.message });
|
|
271
278
|
const failedStatus = {
|
|
272
|
-
status:
|
|
279
|
+
status: 'failed' as const,
|
|
273
280
|
error: e instanceof Error ? e.message : String(e),
|
|
274
|
-
}
|
|
275
|
-
s.status[clientName] = failedStatus
|
|
276
|
-
delete s.clients[clientName]
|
|
277
|
-
})
|
|
281
|
+
};
|
|
282
|
+
s.status[clientName] = failedStatus;
|
|
283
|
+
delete s.clients[clientName];
|
|
284
|
+
});
|
|
278
285
|
if (!tools) {
|
|
279
|
-
continue
|
|
286
|
+
continue;
|
|
280
287
|
}
|
|
281
288
|
for (const [toolName, tool] of Object.entries(tools)) {
|
|
282
|
-
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g,
|
|
283
|
-
const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g,
|
|
284
|
-
result[sanitizedClientName +
|
|
289
|
+
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
290
|
+
const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
291
|
+
result[sanitizedClientName + '_' + sanitizedToolName] = tool;
|
|
285
292
|
}
|
|
286
293
|
}
|
|
287
|
-
return result
|
|
294
|
+
return result;
|
|
288
295
|
}
|
|
289
296
|
}
|