@ottocode/sdk 0.1.205 → 0.1.207
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/package.json +1 -1
- package/src/auth/src/copilot-oauth.ts +11 -4
- package/src/core/src/index.ts +1 -1
- package/src/core/src/mcp/client.ts +28 -3
- package/src/core/src/mcp/index.ts +8 -0
- package/src/core/src/mcp/lazy-tools.ts +89 -0
- package/src/core/src/mcp/server-manager.ts +181 -4
- package/src/core/src/mcp/tools.ts +35 -2
- package/src/core/src/tools/loader.ts +23 -6
- package/src/index.ts +1 -1
- package/src/types/src/auth.ts +1 -0
package/package.json
CHANGED
|
@@ -6,6 +6,10 @@ const POLLING_SAFETY_MARGIN_MS = 3000;
|
|
|
6
6
|
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
7
7
|
const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
8
8
|
|
|
9
|
+
const COPILOT_DEFAULT_SCOPE = 'read:user';
|
|
10
|
+
const COPILOT_MCP_SCOPE =
|
|
11
|
+
'repo read:org read:packages gist notifications read:project security_events';
|
|
12
|
+
|
|
9
13
|
export type CopilotDeviceCodeResponse = {
|
|
10
14
|
verification_uri: string;
|
|
11
15
|
user_code: string;
|
|
@@ -41,7 +45,9 @@ async function openBrowser(url: string) {
|
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export async function requestDeviceCode(
|
|
48
|
+
export async function requestDeviceCode(
|
|
49
|
+
scope?: string,
|
|
50
|
+
): Promise<CopilotDeviceCodeResponse> {
|
|
45
51
|
const response = await fetch(DEVICE_CODE_URL, {
|
|
46
52
|
method: 'POST',
|
|
47
53
|
headers: {
|
|
@@ -50,7 +56,7 @@ export async function requestDeviceCode(): Promise<CopilotDeviceCodeResponse> {
|
|
|
50
56
|
},
|
|
51
57
|
body: JSON.stringify({
|
|
52
58
|
client_id: CLIENT_ID,
|
|
53
|
-
scope:
|
|
59
|
+
scope: scope ?? COPILOT_DEFAULT_SCOPE,
|
|
54
60
|
}),
|
|
55
61
|
});
|
|
56
62
|
|
|
@@ -120,13 +126,14 @@ export async function pollForToken(
|
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
export async function authorizeCopilot(): Promise<{
|
|
129
|
+
export async function authorizeCopilot(options?: { mcp?: boolean }): Promise<{
|
|
124
130
|
verificationUri: string;
|
|
125
131
|
userCode: string;
|
|
126
132
|
deviceCode: string;
|
|
127
133
|
interval: number;
|
|
128
134
|
}> {
|
|
129
|
-
const
|
|
135
|
+
const scope = options?.mcp ? COPILOT_MCP_SCOPE : COPILOT_DEFAULT_SCOPE;
|
|
136
|
+
const deviceData = await requestDeviceCode(scope);
|
|
130
137
|
return {
|
|
131
138
|
verificationUri: deviceData.verification_uri,
|
|
132
139
|
userCode: deviceData.user_code,
|
package/src/core/src/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type { ProviderId, ModelInfo } from '../../types/src/index.ts';
|
|
|
30
30
|
// Tools
|
|
31
31
|
// =======================
|
|
32
32
|
export { discoverProjectTools } from './tools/loader';
|
|
33
|
-
export type { DiscoveredTool } from './tools/loader';
|
|
33
|
+
export type { DiscoveredTool, DiscoverResult } from './tools/loader';
|
|
34
34
|
export { setTerminalManager, getTerminalManager } from './tools/loader';
|
|
35
35
|
|
|
36
36
|
// Tool error handling utilities
|
|
@@ -166,15 +166,18 @@ export class MCPClientWrapper {
|
|
|
166
166
|
args: Record<string, unknown>,
|
|
167
167
|
): Promise<unknown> {
|
|
168
168
|
const result = await this.client.callTool({ name, arguments: args });
|
|
169
|
+
const images = extractImages(result.content);
|
|
169
170
|
if (result.isError) {
|
|
170
171
|
return {
|
|
171
172
|
ok: false,
|
|
172
173
|
error: formatContent(result.content),
|
|
174
|
+
...(images.length > 0 && { images }),
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
return {
|
|
176
178
|
ok: true,
|
|
177
179
|
result: formatContent(result.content),
|
|
180
|
+
...(images.length > 0 && { images }),
|
|
178
181
|
};
|
|
179
182
|
}
|
|
180
183
|
|
|
@@ -218,12 +221,34 @@ function formatContent(content: unknown): string {
|
|
|
218
221
|
if (item && typeof item === 'object' && 'text' in item) {
|
|
219
222
|
parts.push(String(item.text));
|
|
220
223
|
} else if (item && typeof item === 'object' && 'data' in item) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
const mimeType = (item as { mimeType?: string }).mimeType ?? 'unknown';
|
|
225
|
+
if (mimeType.startsWith('image/')) {
|
|
226
|
+
parts.push(`[image: ${mimeType}]`);
|
|
227
|
+
} else {
|
|
228
|
+
parts.push(`[binary data: ${mimeType}]`);
|
|
229
|
+
}
|
|
224
230
|
} else {
|
|
225
231
|
parts.push(JSON.stringify(item));
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
234
|
return parts.join('\n');
|
|
229
235
|
}
|
|
236
|
+
|
|
237
|
+
function extractImages(
|
|
238
|
+
content: unknown,
|
|
239
|
+
): Array<{ data: string; mimeType: string }> {
|
|
240
|
+
if (!Array.isArray(content)) return [];
|
|
241
|
+
const images: Array<{ data: string; mimeType: string }> = [];
|
|
242
|
+
for (const item of content) {
|
|
243
|
+
if (item && typeof item === 'object' && 'data' in item) {
|
|
244
|
+
const mimeType = (item as { mimeType?: string }).mimeType ?? 'unknown';
|
|
245
|
+
if (mimeType.startsWith('image/')) {
|
|
246
|
+
images.push({
|
|
247
|
+
data: String((item as { data: unknown }).data),
|
|
248
|
+
mimeType,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return images;
|
|
254
|
+
}
|
|
@@ -13,6 +13,14 @@ export { MCPServerManager } from './server-manager.ts';
|
|
|
13
13
|
|
|
14
14
|
export { convertMCPToolsToAISDK } from './tools.ts';
|
|
15
15
|
|
|
16
|
+
export {
|
|
17
|
+
getMCPToolBriefs,
|
|
18
|
+
buildLoadMCPToolsTool,
|
|
19
|
+
getMCPToolsRecord,
|
|
20
|
+
buildMCPToolCatalogDescription,
|
|
21
|
+
type MCPToolBrief,
|
|
22
|
+
} from './lazy-tools.ts';
|
|
23
|
+
|
|
16
24
|
export {
|
|
17
25
|
getMCPManager,
|
|
18
26
|
initializeMCP,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
|
+
import type { MCPServerManager } from './server-manager.ts';
|
|
4
|
+
import { convertMCPToolsToAISDK } from './tools.ts';
|
|
5
|
+
|
|
6
|
+
export type MCPToolBrief = {
|
|
7
|
+
name: string;
|
|
8
|
+
server: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function getMCPToolBriefs(manager: MCPServerManager): MCPToolBrief[] {
|
|
13
|
+
return manager.getTools().map(({ name, server, tool: t }) => ({
|
|
14
|
+
name,
|
|
15
|
+
server,
|
|
16
|
+
description: t.description ?? `MCP tool: ${t.name}`,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildMCPToolCatalogDescription(briefs: MCPToolBrief[]): string {
|
|
21
|
+
if (briefs.length === 0) return 'No MCP tools available.';
|
|
22
|
+
const grouped = new Map<string, MCPToolBrief[]>();
|
|
23
|
+
for (const b of briefs) {
|
|
24
|
+
const list = grouped.get(b.server) ?? [];
|
|
25
|
+
list.push(b);
|
|
26
|
+
grouped.set(b.server, list);
|
|
27
|
+
}
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
for (const [server, tools] of grouped) {
|
|
30
|
+
lines.push(`[${server}]`);
|
|
31
|
+
for (const t of tools) {
|
|
32
|
+
lines.push(` ${t.name}: ${t.description.slice(0, 120)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildLoadMCPToolsTool(briefs: MCPToolBrief[]): {
|
|
39
|
+
name: string;
|
|
40
|
+
tool: Tool;
|
|
41
|
+
} {
|
|
42
|
+
const catalog = buildMCPToolCatalogDescription(briefs);
|
|
43
|
+
const validNames = new Set(briefs.map((b) => b.name));
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
name: 'load_mcp_tools',
|
|
47
|
+
tool: tool({
|
|
48
|
+
description: `Load MCP tools by name so they become available for use in the next step. Call this with the tool names you need before using them.\n\nAvailable MCP tools:\n${catalog}`,
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
tools: z
|
|
51
|
+
.array(z.string())
|
|
52
|
+
.describe(
|
|
53
|
+
'Array of MCP tool names to load (e.g. ["chrome__click", "chrome__screenshot"])',
|
|
54
|
+
),
|
|
55
|
+
}),
|
|
56
|
+
execute: async ({ tools: requested }) => {
|
|
57
|
+
const loaded: string[] = [];
|
|
58
|
+
const notFound: string[] = [];
|
|
59
|
+
for (const name of requested) {
|
|
60
|
+
if (validNames.has(name)) {
|
|
61
|
+
loaded.push(name);
|
|
62
|
+
} else {
|
|
63
|
+
notFound.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
loaded,
|
|
69
|
+
...(notFound.length > 0 ? { notFound } : {}),
|
|
70
|
+
message:
|
|
71
|
+
loaded.length > 0
|
|
72
|
+
? `Loaded ${loaded.length} tool(s). They are now available for use.`
|
|
73
|
+
: 'No valid tools to load.',
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getMCPToolsRecord(
|
|
81
|
+
manager: MCPServerManager,
|
|
82
|
+
): Record<string, Tool> {
|
|
83
|
+
const mcpTools = convertMCPToolsToAISDK(manager);
|
|
84
|
+
const record: Record<string, Tool> = {};
|
|
85
|
+
for (const { name, tool: t } of mcpTools) {
|
|
86
|
+
record[name] = t;
|
|
87
|
+
}
|
|
88
|
+
return record;
|
|
89
|
+
}
|
|
@@ -3,6 +3,65 @@ import type { MCPServerConfig, MCPServerStatus } from './types.ts';
|
|
|
3
3
|
import { OAuthCredentialStore } from './oauth/store.ts';
|
|
4
4
|
import { OttoOAuthProvider } from './oauth/provider.ts';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
+
import { getAuth } from '../../../auth/src/index.ts';
|
|
7
|
+
|
|
8
|
+
const GITHUB_COPILOT_HOSTS = [
|
|
9
|
+
'api.githubcopilot.com',
|
|
10
|
+
'copilot-proxy.githubusercontent.com',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function isGitHubCopilotUrl(url?: string): boolean {
|
|
14
|
+
if (!url) return false;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(url);
|
|
17
|
+
return GITHUB_COPILOT_HOSTS.some(
|
|
18
|
+
(h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`),
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COPILOT_MCP_REQUIRED_SCOPES = [
|
|
26
|
+
'repo',
|
|
27
|
+
'read:org',
|
|
28
|
+
'gist',
|
|
29
|
+
'notifications',
|
|
30
|
+
'read:project',
|
|
31
|
+
'security_events',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function hasMCPScopes(scopes?: string): boolean {
|
|
35
|
+
if (!scopes) return false;
|
|
36
|
+
const granted = scopes.split(/[\s,]+/).filter(Boolean);
|
|
37
|
+
return COPILOT_MCP_REQUIRED_SCOPES.every((s) => granted.includes(s));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function getCopilotToken(): Promise<string | null> {
|
|
41
|
+
try {
|
|
42
|
+
const auth = await getAuth('copilot');
|
|
43
|
+
if (auth?.type === 'oauth' && auth.refresh) {
|
|
44
|
+
return auth.refresh;
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getCopilotMCPToken(): Promise<{
|
|
51
|
+
token: string | null;
|
|
52
|
+
needsReauth: boolean;
|
|
53
|
+
}> {
|
|
54
|
+
try {
|
|
55
|
+
const auth = await getAuth('copilot');
|
|
56
|
+
if (auth?.type === 'oauth' && auth.refresh) {
|
|
57
|
+
if (!hasMCPScopes(auth.scopes)) {
|
|
58
|
+
return { token: auth.refresh, needsReauth: true };
|
|
59
|
+
}
|
|
60
|
+
return { token: auth.refresh, needsReauth: false };
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return { token: null, needsReauth: true };
|
|
64
|
+
}
|
|
6
65
|
|
|
7
66
|
type IndexedTool = {
|
|
8
67
|
server: string;
|
|
@@ -55,6 +114,57 @@ export class MCPServerManager {
|
|
|
55
114
|
const transport = config.transport ?? 'stdio';
|
|
56
115
|
|
|
57
116
|
if (transport !== 'stdio') {
|
|
117
|
+
if (isGitHubCopilotUrl(config.url)) {
|
|
118
|
+
const { token, needsReauth } = await getCopilotMCPToken();
|
|
119
|
+
if (token && !needsReauth) {
|
|
120
|
+
config = {
|
|
121
|
+
...config,
|
|
122
|
+
headers: {
|
|
123
|
+
...config.headers,
|
|
124
|
+
Authorization: `Bearer ${token}`,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const updatedClient = new MCPClientWrapper(config);
|
|
128
|
+
try {
|
|
129
|
+
await updatedClient.connect();
|
|
130
|
+
this.clients.set(config.name, updatedClient);
|
|
131
|
+
const tools = await updatedClient.listTools();
|
|
132
|
+
for (const tool of tools) {
|
|
133
|
+
const fullName = `${config.name}__${tool.name}`;
|
|
134
|
+
this.toolsMap.set(fullName, { server: config.name, tool });
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
this.clients.set(config.name, updatedClient);
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
if (
|
|
140
|
+
msg.includes('insufficient scopes') ||
|
|
141
|
+
msg.includes('Forbidden')
|
|
142
|
+
) {
|
|
143
|
+
console.error(
|
|
144
|
+
`[mcp] GitHub Copilot MCP server "${config.name}" has insufficient scopes. Run \`otto mcp auth ${config.name}\` to re-authenticate with required permissions.`,
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
console.error(
|
|
148
|
+
`[mcp] Failed to start server "${config.name}": ${msg}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (token && needsReauth) {
|
|
155
|
+
console.error(
|
|
156
|
+
`[mcp] GitHub Copilot MCP server "${config.name}" needs broader permissions. Run \`otto mcp auth ${config.name}\` to re-authenticate.`,
|
|
157
|
+
);
|
|
158
|
+
this.clients.set(config.name, client);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.error(
|
|
162
|
+
`[mcp] GitHub Copilot MCP server "${config.name}" requires authentication. Run \`otto auth login copilot\` or \`otto mcp auth ${config.name}\`.`,
|
|
163
|
+
);
|
|
164
|
+
this.clients.set(config.name, client);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
58
168
|
const hasStaticAuth =
|
|
59
169
|
config.headers?.Authorization || config.headers?.authorization;
|
|
60
170
|
if (!hasStaticAuth) {
|
|
@@ -184,10 +294,15 @@ export class MCPServerManager {
|
|
|
184
294
|
.filter(([, v]) => v.server === name)
|
|
185
295
|
.map(([k]) => k);
|
|
186
296
|
const config = client.serverConfig;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
297
|
+
let authenticated = false;
|
|
298
|
+
if (isGitHubCopilotUrl(config.url)) {
|
|
299
|
+
authenticated = !!(await getCopilotToken());
|
|
300
|
+
} else {
|
|
301
|
+
const key = this.oauthKey(name);
|
|
302
|
+
authenticated = await this.oauthStore
|
|
303
|
+
.isAuthenticated(key)
|
|
304
|
+
.catch(() => false);
|
|
305
|
+
}
|
|
191
306
|
|
|
192
307
|
statuses.push({
|
|
193
308
|
name,
|
|
@@ -218,8 +333,40 @@ export class MCPServerManager {
|
|
|
218
333
|
const transport = config.transport ?? 'stdio';
|
|
219
334
|
if (transport === 'stdio') return null;
|
|
220
335
|
|
|
336
|
+
if (isGitHubCopilotUrl(config.url)) {
|
|
337
|
+
const token = await getCopilotToken();
|
|
338
|
+
if (token) {
|
|
339
|
+
const authedConfig = {
|
|
340
|
+
...config,
|
|
341
|
+
headers: {
|
|
342
|
+
...config.headers,
|
|
343
|
+
Authorization: `Bearer ${token}`,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
const client = new MCPClientWrapper(authedConfig);
|
|
347
|
+
try {
|
|
348
|
+
await client.connect();
|
|
349
|
+
this.clients.set(config.name, client);
|
|
350
|
+
const tools = await client.listTools();
|
|
351
|
+
for (const tool of tools) {
|
|
352
|
+
const fullName = `${config.name}__${tool.name}`;
|
|
353
|
+
this.toolsMap.set(fullName, { server: config.name, tool });
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
this.clients.set(config.name, client);
|
|
357
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
358
|
+
console.error(
|
|
359
|
+
`[mcp] Failed to start server "${config.name}": ${msg}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
221
367
|
this.serverScopes.set(config.name, config.scope ?? 'global');
|
|
222
368
|
const key = this.oauthKey(config.name);
|
|
369
|
+
await this.oauthStore.clearServer(key);
|
|
223
370
|
const provider = new OttoOAuthProvider(key, this.oauthStore, {
|
|
224
371
|
clientId: config.oauth?.clientId,
|
|
225
372
|
callbackPort: config.oauth?.callbackPort,
|
|
@@ -244,6 +391,7 @@ export class MCPServerManager {
|
|
|
244
391
|
|
|
245
392
|
if (provider.pendingAuthUrl) {
|
|
246
393
|
this.pendingAuth.set(config.name, provider.pendingAuthUrl);
|
|
394
|
+
this.waitForAuthAndReconnect(config.name, provider);
|
|
247
395
|
return provider.pendingAuthUrl;
|
|
248
396
|
}
|
|
249
397
|
return null;
|
|
@@ -295,9 +443,38 @@ export class MCPServerManager {
|
|
|
295
443
|
await this.stopServer(name);
|
|
296
444
|
}
|
|
297
445
|
|
|
446
|
+
async clearAuthData(
|
|
447
|
+
name: string,
|
|
448
|
+
scope?: 'global' | 'project',
|
|
449
|
+
projectRoot?: string,
|
|
450
|
+
): Promise<void> {
|
|
451
|
+
const provider = this.authProviders.get(name);
|
|
452
|
+
if (provider) {
|
|
453
|
+
await provider.clearCredentials();
|
|
454
|
+
provider.cleanup();
|
|
455
|
+
}
|
|
456
|
+
this.authProviders.delete(name);
|
|
457
|
+
if (scope) {
|
|
458
|
+
this.serverScopes.set(name, scope);
|
|
459
|
+
}
|
|
460
|
+
if (projectRoot) {
|
|
461
|
+
this.projectRoot = projectRoot;
|
|
462
|
+
}
|
|
463
|
+
const key = this.oauthKey(name);
|
|
464
|
+
await this.oauthStore.clearServer(key);
|
|
465
|
+
if (key !== name) {
|
|
466
|
+
await this.oauthStore.clearServer(name);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
298
470
|
async getAuthStatus(
|
|
299
471
|
name: string,
|
|
300
472
|
): Promise<{ authenticated: boolean; expiresAt?: number }> {
|
|
473
|
+
const client = this.clients.get(name);
|
|
474
|
+
if (client && isGitHubCopilotUrl(client.serverConfig.url)) {
|
|
475
|
+
const token = await getCopilotToken();
|
|
476
|
+
return { authenticated: !!token };
|
|
477
|
+
}
|
|
301
478
|
const key = this.oauthKey(name);
|
|
302
479
|
const tokens = await this.oauthStore.loadTokens(key);
|
|
303
480
|
if (!tokens?.access_token) return { authenticated: false };
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { tool, type Tool } from 'ai';
|
|
2
|
+
import type { ToolResultOutput } from '@ai-sdk/provider-utils';
|
|
2
3
|
import { z } from 'zod/v3';
|
|
3
4
|
import type { MCPServerManager } from './server-manager.ts';
|
|
4
5
|
|
|
6
|
+
type MCPToolResult = {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
result?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
images?: Array<{ data: string; mimeType: string }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
5
13
|
export function convertMCPToolsToAISDK(
|
|
6
14
|
manager: MCPServerManager,
|
|
7
15
|
): Array<{ name: string; tool: Tool }> {
|
|
@@ -16,13 +24,38 @@ export function convertMCPToolsToAISDK(
|
|
|
16
24
|
) as z.ZodObject<z.ZodRawShape>,
|
|
17
25
|
async execute(args: Record<string, unknown>) {
|
|
18
26
|
try {
|
|
19
|
-
return await manager.callTool(name, args);
|
|
27
|
+
return (await manager.callTool(name, args)) as MCPToolResult;
|
|
20
28
|
} catch (err) {
|
|
21
29
|
return {
|
|
22
30
|
ok: false,
|
|
23
31
|
error: err instanceof Error ? err.message : String(err),
|
|
24
|
-
};
|
|
32
|
+
} satisfies MCPToolResult;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
toModelOutput({ output }): ToolResultOutput {
|
|
36
|
+
const result = output as MCPToolResult;
|
|
37
|
+
if (result.images && result.images.length > 0) {
|
|
38
|
+
const parts: Array<
|
|
39
|
+
| { type: 'text'; text: string }
|
|
40
|
+
| { type: 'image-data'; data: string; mediaType: string }
|
|
41
|
+
> = [];
|
|
42
|
+
const text = result.ok ? result.result : result.error;
|
|
43
|
+
if (text) {
|
|
44
|
+
parts.push({ type: 'text', text });
|
|
45
|
+
}
|
|
46
|
+
for (const img of result.images) {
|
|
47
|
+
parts.push({
|
|
48
|
+
type: 'image-data',
|
|
49
|
+
data: img.data,
|
|
50
|
+
mediaType: img.mimeType,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { type: 'content', value: parts } as ToolResultOutput;
|
|
25
54
|
}
|
|
55
|
+
return {
|
|
56
|
+
type: 'json',
|
|
57
|
+
value: result as unknown as import('@ai-sdk/provider').JSONValue,
|
|
58
|
+
};
|
|
26
59
|
},
|
|
27
60
|
}),
|
|
28
61
|
}));
|
|
@@ -16,7 +16,12 @@ import { buildTerminalTool } from './builtin/terminal.ts';
|
|
|
16
16
|
import type { TerminalManager } from '../terminals/index.ts';
|
|
17
17
|
import { initializeSkills, buildSkillTool } from '../../../skills/index.ts';
|
|
18
18
|
import { getMCPManager } from '../mcp/index.ts';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getMCPToolBriefs,
|
|
21
|
+
buildLoadMCPToolsTool,
|
|
22
|
+
getMCPToolsRecord,
|
|
23
|
+
type MCPToolBrief,
|
|
24
|
+
} from '../mcp/lazy-tools.ts';
|
|
20
25
|
import fg from 'fast-glob';
|
|
21
26
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
22
27
|
import { pathToFileURL } from 'node:url';
|
|
@@ -25,6 +30,11 @@ import { spawn as nodeSpawn } from 'node:child_process';
|
|
|
25
30
|
|
|
26
31
|
export type DiscoveredTool = { name: string; tool: Tool };
|
|
27
32
|
|
|
33
|
+
export type DiscoverResult = {
|
|
34
|
+
tools: DiscoveredTool[];
|
|
35
|
+
mcpToolsRecord: Record<string, Tool>;
|
|
36
|
+
};
|
|
37
|
+
|
|
28
38
|
type PluginParameter = {
|
|
29
39
|
type: 'string' | 'number' | 'boolean';
|
|
30
40
|
description?: string;
|
|
@@ -108,7 +118,7 @@ export function getTerminalManager(): TerminalManager | null {
|
|
|
108
118
|
export async function discoverProjectTools(
|
|
109
119
|
projectRoot: string,
|
|
110
120
|
globalConfigDir?: string,
|
|
111
|
-
): Promise<
|
|
121
|
+
): Promise<DiscoverResult> {
|
|
112
122
|
const tools = new Map<string, Tool>();
|
|
113
123
|
for (const { name, tool } of buildFsTools(projectRoot)) tools.set(name, tool);
|
|
114
124
|
for (const { name, tool } of buildGitTools(projectRoot))
|
|
@@ -148,10 +158,14 @@ export async function discoverProjectTools(
|
|
|
148
158
|
tools.set(skillTool.name, skillTool.tool);
|
|
149
159
|
|
|
150
160
|
const mcpManager = getMCPManager();
|
|
161
|
+
let mcpToolsRecord: Record<string, Tool> = {};
|
|
162
|
+
let mcpBriefs: MCPToolBrief[] = [];
|
|
151
163
|
if (mcpManager?.started) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
mcpBriefs = getMCPToolBriefs(mcpManager);
|
|
165
|
+
if (mcpBriefs.length > 0) {
|
|
166
|
+
mcpToolsRecord = getMCPToolsRecord(mcpManager);
|
|
167
|
+
const loadTool = buildLoadMCPToolsTool(mcpBriefs);
|
|
168
|
+
tools.set(loadTool.name, loadTool.tool);
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
|
|
@@ -210,7 +224,10 @@ export async function discoverProjectTools(
|
|
|
210
224
|
|
|
211
225
|
await loadFromBase(globalConfigDir);
|
|
212
226
|
await loadFromBase(join(projectRoot, '.otto'));
|
|
213
|
-
return
|
|
227
|
+
return {
|
|
228
|
+
tools: Array.from(tools.entries()).map(([name, tool]) => ({ name, tool })),
|
|
229
|
+
mcpToolsRecord,
|
|
230
|
+
};
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
async function loadPlugin(
|
package/src/index.ts
CHANGED
|
@@ -218,7 +218,7 @@ export type { ProviderName, ModelConfig } from './core/src/index.ts';
|
|
|
218
218
|
|
|
219
219
|
// Tools
|
|
220
220
|
export { discoverProjectTools } from './core/src/index.ts';
|
|
221
|
-
export type { DiscoveredTool } from './core/src/index.ts';
|
|
221
|
+
export type { DiscoveredTool, DiscoverResult } from './core/src/index.ts';
|
|
222
222
|
export { setTerminalManager, getTerminalManager } from './core/src/index.ts';
|
|
223
223
|
export { buildFsTools } from './core/src/index.ts';
|
|
224
224
|
export { buildGitTools } from './core/src/index.ts';
|