@ottocode/sdk 0.1.200 → 0.1.201
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 +2 -1
- package/src/core/src/index.ts +29 -0
- package/src/core/src/mcp/client.ts +229 -0
- package/src/core/src/mcp/index.ts +31 -0
- package/src/core/src/mcp/lifecycle.ts +134 -0
- package/src/core/src/mcp/oauth/callback.ts +83 -0
- package/src/core/src/mcp/oauth/index.ts +6 -0
- package/src/core/src/mcp/oauth/provider.ts +149 -0
- package/src/core/src/mcp/oauth/store.ts +121 -0
- package/src/core/src/mcp/server-manager.ts +304 -0
- package/src/core/src/mcp/tools.ts +97 -0
- package/src/core/src/mcp/types.ts +39 -0
- package/src/core/src/tools/builtin/ripgrep.ts +12 -4
- package/src/core/src/tools/loader.ts +10 -3
- package/src/index.ts +29 -0
- package/src/core/src/tools/builtin/grep.ts +0 -134
- package/src/core/src/tools/builtin/grep.txt +0 -9
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface StoredOAuthData {
|
|
5
|
+
tokens?: {
|
|
6
|
+
access_token: string;
|
|
7
|
+
token_type?: string;
|
|
8
|
+
expires_in?: number;
|
|
9
|
+
refresh_token?: string;
|
|
10
|
+
scope?: string;
|
|
11
|
+
expires_at?: number;
|
|
12
|
+
};
|
|
13
|
+
clientInfo?: {
|
|
14
|
+
client_id: string;
|
|
15
|
+
client_secret?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
codeVerifier?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class OAuthCredentialStore {
|
|
22
|
+
private storePath: string;
|
|
23
|
+
|
|
24
|
+
constructor(storePath?: string) {
|
|
25
|
+
this.storePath =
|
|
26
|
+
storePath ??
|
|
27
|
+
join(
|
|
28
|
+
process.env.HOME ?? process.env.USERPROFILE ?? '',
|
|
29
|
+
'.config',
|
|
30
|
+
'otto',
|
|
31
|
+
'oauth',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private filePath(serverName: string): string {
|
|
36
|
+
const safe = serverName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
37
|
+
return join(this.storePath, `${safe}.json`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async read(serverName: string): Promise<StoredOAuthData> {
|
|
41
|
+
try {
|
|
42
|
+
const text = await fs.readFile(this.filePath(serverName), 'utf-8');
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async write(
|
|
50
|
+
serverName: string,
|
|
51
|
+
data: StoredOAuthData,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await fs.mkdir(this.storePath, { recursive: true, mode: 0o700 });
|
|
54
|
+
await fs.writeFile(
|
|
55
|
+
this.filePath(serverName),
|
|
56
|
+
JSON.stringify(data, null, '\t'),
|
|
57
|
+
{ encoding: 'utf-8', mode: 0o600 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async loadTokens(
|
|
62
|
+
serverName: string,
|
|
63
|
+
): Promise<StoredOAuthData['tokens'] | undefined> {
|
|
64
|
+
const data = await this.read(serverName);
|
|
65
|
+
return data.tokens;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async saveTokens(
|
|
69
|
+
serverName: string,
|
|
70
|
+
tokens: StoredOAuthData['tokens'],
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const data = await this.read(serverName);
|
|
73
|
+
data.tokens = tokens;
|
|
74
|
+
await this.write(serverName, data);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async loadClientInfo(
|
|
78
|
+
serverName: string,
|
|
79
|
+
): Promise<StoredOAuthData['clientInfo'] | undefined> {
|
|
80
|
+
const data = await this.read(serverName);
|
|
81
|
+
return data.clientInfo;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async saveClientInfo(
|
|
85
|
+
serverName: string,
|
|
86
|
+
clientInfo: StoredOAuthData['clientInfo'],
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const data = await this.read(serverName);
|
|
89
|
+
data.clientInfo = clientInfo;
|
|
90
|
+
await this.write(serverName, data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async loadCodeVerifier(serverName: string): Promise<string | undefined> {
|
|
94
|
+
const data = await this.read(serverName);
|
|
95
|
+
return data.codeVerifier;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async saveCodeVerifier(
|
|
99
|
+
serverName: string,
|
|
100
|
+
codeVerifier: string,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const data = await this.read(serverName);
|
|
103
|
+
data.codeVerifier = codeVerifier;
|
|
104
|
+
await this.write(serverName, data);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async clearServer(serverName: string): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
await fs.unlink(this.filePath(serverName));
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async isAuthenticated(serverName: string): Promise<boolean> {
|
|
114
|
+
const tokens = await this.loadTokens(serverName);
|
|
115
|
+
if (!tokens?.access_token) return false;
|
|
116
|
+
if (tokens.expires_at && tokens.expires_at < Date.now() / 1000) {
|
|
117
|
+
return !!tokens.refresh_token;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { MCPClientWrapper, type MCPToolInfo } from './client.ts';
|
|
2
|
+
import type { MCPServerConfig, MCPServerStatus } from './types.ts';
|
|
3
|
+
import { OAuthCredentialStore } from './oauth/store.ts';
|
|
4
|
+
import { OttoOAuthProvider } from './oauth/provider.ts';
|
|
5
|
+
|
|
6
|
+
type IndexedTool = {
|
|
7
|
+
server: string;
|
|
8
|
+
tool: MCPToolInfo;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class MCPServerManager {
|
|
12
|
+
private clients = new Map<string, MCPClientWrapper>();
|
|
13
|
+
private toolsMap = new Map<string, IndexedTool>();
|
|
14
|
+
private authProviders = new Map<string, OttoOAuthProvider>();
|
|
15
|
+
private pendingAuth = new Map<string, string>();
|
|
16
|
+
private oauthStore = new OAuthCredentialStore();
|
|
17
|
+
private _started = false;
|
|
18
|
+
|
|
19
|
+
get started(): boolean {
|
|
20
|
+
return this._started;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async startServers(configs: MCPServerConfig[]): Promise<void> {
|
|
24
|
+
await this.stopAll();
|
|
25
|
+
|
|
26
|
+
for (const config of configs) {
|
|
27
|
+
if (config.disabled) continue;
|
|
28
|
+
await this.startSingleServer(config);
|
|
29
|
+
}
|
|
30
|
+
this._started = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async startSingleServer(config: MCPServerConfig): Promise<void> {
|
|
34
|
+
const client = new MCPClientWrapper(config);
|
|
35
|
+
const transport = config.transport ?? 'stdio';
|
|
36
|
+
|
|
37
|
+
if (transport !== 'stdio') {
|
|
38
|
+
const hasStaticAuth =
|
|
39
|
+
config.headers?.Authorization || config.headers?.authorization;
|
|
40
|
+
if (!hasStaticAuth) {
|
|
41
|
+
const provider = new OttoOAuthProvider(config.name, this.oauthStore, {
|
|
42
|
+
clientId: config.oauth?.clientId,
|
|
43
|
+
callbackPort: config.oauth?.callbackPort,
|
|
44
|
+
scopes: config.oauth?.scopes,
|
|
45
|
+
});
|
|
46
|
+
client.setAuthProvider(provider);
|
|
47
|
+
this.authProviders.set(config.name, provider);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await client.connect();
|
|
53
|
+
this.clients.set(config.name, client);
|
|
54
|
+
|
|
55
|
+
const tools = await client.listTools();
|
|
56
|
+
for (const tool of tools) {
|
|
57
|
+
const fullName = `${config.name}__${tool.name}`;
|
|
58
|
+
this.toolsMap.set(fullName, { server: config.name, tool });
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
this.clients.set(config.name, client);
|
|
62
|
+
|
|
63
|
+
if (client.authRequired) {
|
|
64
|
+
const provider = this.authProviders.get(config.name);
|
|
65
|
+
if (provider?.pendingAuthUrl) {
|
|
66
|
+
this.pendingAuth.set(config.name, provider.pendingAuthUrl);
|
|
67
|
+
this.waitForAuthAndReconnect(config.name, provider);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
console.error(`[mcp] Failed to start server "${config.name}": ${msg}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private waitForAuthAndReconnect(
|
|
78
|
+
name: string,
|
|
79
|
+
provider: OttoOAuthProvider,
|
|
80
|
+
): void {
|
|
81
|
+
provider
|
|
82
|
+
.waitForAuthCode()
|
|
83
|
+
.then(async (code) => {
|
|
84
|
+
console.log(`[mcp] Auth code received for "${name}", reconnecting...`);
|
|
85
|
+
const success = await this.completeAuth(name, code);
|
|
86
|
+
if (success) {
|
|
87
|
+
console.log(`[mcp] Successfully authenticated "${name}"`);
|
|
88
|
+
} else {
|
|
89
|
+
console.error(`[mcp] Failed to complete auth for "${name}"`);
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async stopAll(): Promise<void> {
|
|
96
|
+
for (const provider of this.authProviders.values()) {
|
|
97
|
+
provider.cleanup();
|
|
98
|
+
}
|
|
99
|
+
const disconnects = Array.from(this.clients.values()).map((c) =>
|
|
100
|
+
c.disconnect().catch(() => {}),
|
|
101
|
+
);
|
|
102
|
+
await Promise.all(disconnects);
|
|
103
|
+
this.clients.clear();
|
|
104
|
+
this.toolsMap.clear();
|
|
105
|
+
this.authProviders.clear();
|
|
106
|
+
this.pendingAuth.clear();
|
|
107
|
+
this._started = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getTools(): Array<{ name: string; server: string; tool: MCPToolInfo }> {
|
|
111
|
+
return Array.from(this.toolsMap.entries()).map(
|
|
112
|
+
([name, { server, tool }]) => ({
|
|
113
|
+
name,
|
|
114
|
+
server,
|
|
115
|
+
tool,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async callTool(
|
|
121
|
+
fullName: string,
|
|
122
|
+
args: Record<string, unknown>,
|
|
123
|
+
): Promise<unknown> {
|
|
124
|
+
const entry = this.toolsMap.get(fullName);
|
|
125
|
+
if (!entry) throw new Error(`Unknown MCP tool: ${fullName}`);
|
|
126
|
+
|
|
127
|
+
const client = this.clients.get(entry.server);
|
|
128
|
+
if (!client) throw new Error(`MCP server not connected: ${entry.server}`);
|
|
129
|
+
|
|
130
|
+
return client.callTool(entry.tool.name, args);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getStatus(): MCPServerStatus[] {
|
|
134
|
+
const statuses: MCPServerStatus[] = [];
|
|
135
|
+
for (const [name, client] of this.clients) {
|
|
136
|
+
const tools = Array.from(this.toolsMap.entries())
|
|
137
|
+
.filter(([, v]) => v.server === name)
|
|
138
|
+
.map(([k]) => k);
|
|
139
|
+
const config = client.serverConfig;
|
|
140
|
+
const _authenticated = this.oauthStore
|
|
141
|
+
.isAuthenticated(name)
|
|
142
|
+
.catch(() => false);
|
|
143
|
+
|
|
144
|
+
statuses.push({
|
|
145
|
+
name,
|
|
146
|
+
connected: client.connected,
|
|
147
|
+
tools,
|
|
148
|
+
transport: config.transport,
|
|
149
|
+
url: config.url,
|
|
150
|
+
authRequired: client.authRequired,
|
|
151
|
+
authenticated: false,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return statuses;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getStatusAsync(): Promise<MCPServerStatus[]> {
|
|
158
|
+
const statuses: MCPServerStatus[] = [];
|
|
159
|
+
for (const [name, client] of this.clients) {
|
|
160
|
+
const tools = Array.from(this.toolsMap.entries())
|
|
161
|
+
.filter(([, v]) => v.server === name)
|
|
162
|
+
.map(([k]) => k);
|
|
163
|
+
const config = client.serverConfig;
|
|
164
|
+
const authenticated = await this.oauthStore
|
|
165
|
+
.isAuthenticated(name)
|
|
166
|
+
.catch(() => false);
|
|
167
|
+
|
|
168
|
+
statuses.push({
|
|
169
|
+
name,
|
|
170
|
+
connected: client.connected,
|
|
171
|
+
tools,
|
|
172
|
+
transport: config.transport,
|
|
173
|
+
url: config.url,
|
|
174
|
+
authRequired: client.authRequired,
|
|
175
|
+
authenticated,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return statuses;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getServerNames(): string[] {
|
|
182
|
+
return Array.from(this.clients.keys());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
isServerConnected(name: string): boolean {
|
|
186
|
+
return this.clients.get(name)?.connected ?? false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getAuthUrl(name: string): string | null {
|
|
190
|
+
return this.pendingAuth.get(name) ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async initiateAuth(config: MCPServerConfig): Promise<string | null> {
|
|
194
|
+
const transport = config.transport ?? 'stdio';
|
|
195
|
+
if (transport === 'stdio') return null;
|
|
196
|
+
|
|
197
|
+
const provider = new OttoOAuthProvider(config.name, this.oauthStore, {
|
|
198
|
+
clientId: config.oauth?.clientId,
|
|
199
|
+
callbackPort: config.oauth?.callbackPort,
|
|
200
|
+
scopes: config.oauth?.scopes,
|
|
201
|
+
});
|
|
202
|
+
this.authProviders.set(config.name, provider);
|
|
203
|
+
|
|
204
|
+
const client = new MCPClientWrapper(config);
|
|
205
|
+
client.setAuthProvider(provider);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await client.connect();
|
|
209
|
+
this.clients.set(config.name, client);
|
|
210
|
+
const tools = await client.listTools();
|
|
211
|
+
for (const tool of tools) {
|
|
212
|
+
const fullName = `${config.name}__${tool.name}`;
|
|
213
|
+
this.toolsMap.set(fullName, { server: config.name, tool });
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
} catch {
|
|
217
|
+
this.clients.set(config.name, client);
|
|
218
|
+
|
|
219
|
+
if (provider.pendingAuthUrl) {
|
|
220
|
+
this.pendingAuth.set(config.name, provider.pendingAuthUrl);
|
|
221
|
+
return provider.pendingAuthUrl;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async completeAuth(name: string, code: string): Promise<boolean> {
|
|
228
|
+
const client = this.clients.get(name);
|
|
229
|
+
const provider = this.authProviders.get(name);
|
|
230
|
+
if (!client || !provider) return false;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await client.finishAuth(code);
|
|
234
|
+
await client.disconnect();
|
|
235
|
+
|
|
236
|
+
const config = client.serverConfig;
|
|
237
|
+
const newClient = new MCPClientWrapper(config);
|
|
238
|
+
newClient.setAuthProvider(provider);
|
|
239
|
+
await newClient.connect();
|
|
240
|
+
|
|
241
|
+
this.clients.set(name, newClient);
|
|
242
|
+
|
|
243
|
+
const tools = await newClient.listTools();
|
|
244
|
+
for (const [key, val] of this.toolsMap) {
|
|
245
|
+
if (val.server === name) this.toolsMap.delete(key);
|
|
246
|
+
}
|
|
247
|
+
for (const tool of tools) {
|
|
248
|
+
const fullName = `${name}__${tool.name}`;
|
|
249
|
+
this.toolsMap.set(fullName, { server: name, tool });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.pendingAuth.delete(name);
|
|
253
|
+
provider.cleanup();
|
|
254
|
+
return true;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
console.error(`[mcp] Failed to complete auth for "${name}": ${msg}`);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async revokeAuth(name: string): Promise<void> {
|
|
263
|
+
const provider = this.authProviders.get(name);
|
|
264
|
+
if (provider) {
|
|
265
|
+
await provider.clearCredentials();
|
|
266
|
+
provider.cleanup();
|
|
267
|
+
}
|
|
268
|
+
this.authProviders.delete(name);
|
|
269
|
+
await this.stopServer(name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getAuthStatus(
|
|
273
|
+
name: string,
|
|
274
|
+
): Promise<{ authenticated: boolean; expiresAt?: number }> {
|
|
275
|
+
const tokens = await this.oauthStore.loadTokens(name);
|
|
276
|
+
if (!tokens?.access_token) return { authenticated: false };
|
|
277
|
+
return {
|
|
278
|
+
authenticated: true,
|
|
279
|
+
expiresAt: tokens.expires_at,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async restartServer(config: MCPServerConfig): Promise<void> {
|
|
284
|
+
await this.stopServer(config.name);
|
|
285
|
+
await this.startSingleServer(config);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async stopServer(name: string): Promise<void> {
|
|
289
|
+
const provider = this.authProviders.get(name);
|
|
290
|
+
if (provider) {
|
|
291
|
+
provider.cleanup();
|
|
292
|
+
this.authProviders.delete(name);
|
|
293
|
+
}
|
|
294
|
+
const client = this.clients.get(name);
|
|
295
|
+
if (client) {
|
|
296
|
+
await client.disconnect().catch(() => {});
|
|
297
|
+
this.clients.delete(name);
|
|
298
|
+
for (const [key, val] of this.toolsMap) {
|
|
299
|
+
if (val.server === name) this.toolsMap.delete(key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.pendingAuth.delete(name);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
|
+
import type { MCPServerManager } from './server-manager.ts';
|
|
4
|
+
|
|
5
|
+
export function convertMCPToolsToAISDK(
|
|
6
|
+
manager: MCPServerManager,
|
|
7
|
+
): Array<{ name: string; tool: Tool }> {
|
|
8
|
+
const mcpTools = manager.getTools();
|
|
9
|
+
|
|
10
|
+
return mcpTools.map(({ name, tool: mcpTool }) => ({
|
|
11
|
+
name,
|
|
12
|
+
tool: tool({
|
|
13
|
+
description: mcpTool.description ?? `MCP tool: ${mcpTool.name}`,
|
|
14
|
+
inputSchema: jsonSchemaToZod(
|
|
15
|
+
mcpTool.inputSchema,
|
|
16
|
+
) as z.ZodObject<z.ZodRawShape>,
|
|
17
|
+
async execute(args: Record<string, unknown>) {
|
|
18
|
+
try {
|
|
19
|
+
return await manager.callTool(name, args);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
error: err instanceof Error ? err.message : String(err),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type JSONSchema = {
|
|
32
|
+
type?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
enum?: string[];
|
|
35
|
+
properties?: Record<string, JSONSchema>;
|
|
36
|
+
required?: string[];
|
|
37
|
+
items?: JSONSchema;
|
|
38
|
+
default?: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function jsonSchemaToZod(schema: Record<string, unknown>): z.ZodTypeAny {
|
|
42
|
+
const properties = schema.properties as
|
|
43
|
+
| Record<string, JSONSchema>
|
|
44
|
+
| undefined;
|
|
45
|
+
if (!properties) return z.object({});
|
|
46
|
+
|
|
47
|
+
const required = new Set((schema.required as string[]) ?? []);
|
|
48
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
49
|
+
|
|
50
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
51
|
+
let field = convertProperty(prop);
|
|
52
|
+
if (!required.has(key)) field = field.optional();
|
|
53
|
+
shape[key] = field;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return z.object(shape);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function convertProperty(prop: JSONSchema): z.ZodTypeAny {
|
|
60
|
+
if (prop.enum) {
|
|
61
|
+
const enumSchema = z.enum(prop.enum as [string, ...string[]]);
|
|
62
|
+
return prop.description
|
|
63
|
+
? enumSchema.describe(prop.description)
|
|
64
|
+
: enumSchema;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
switch (prop.type) {
|
|
68
|
+
case 'string': {
|
|
69
|
+
const s = z.string();
|
|
70
|
+
return prop.description ? s.describe(prop.description) : s;
|
|
71
|
+
}
|
|
72
|
+
case 'number': {
|
|
73
|
+
const n = z.number();
|
|
74
|
+
return prop.description ? n.describe(prop.description) : n;
|
|
75
|
+
}
|
|
76
|
+
case 'integer': {
|
|
77
|
+
const i = z.number().int();
|
|
78
|
+
return prop.description ? i.describe(prop.description) : i;
|
|
79
|
+
}
|
|
80
|
+
case 'boolean': {
|
|
81
|
+
const b = z.boolean();
|
|
82
|
+
return prop.description ? b.describe(prop.description) : b;
|
|
83
|
+
}
|
|
84
|
+
case 'array': {
|
|
85
|
+
const items = prop.items ? convertProperty(prop.items) : z.unknown();
|
|
86
|
+
const a = z.array(items);
|
|
87
|
+
return prop.description ? a.describe(prop.description) : a;
|
|
88
|
+
}
|
|
89
|
+
case 'object': {
|
|
90
|
+
return jsonSchemaToZod(prop as Record<string, unknown>);
|
|
91
|
+
}
|
|
92
|
+
default: {
|
|
93
|
+
const u = z.unknown();
|
|
94
|
+
return prop.description ? u.describe(prop.description) : u;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type MCPTransport = 'stdio' | 'http' | 'sse';
|
|
2
|
+
|
|
3
|
+
export interface MCPOAuthConfig {
|
|
4
|
+
clientId?: string;
|
|
5
|
+
callbackPort?: number;
|
|
6
|
+
scopes?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MCPServerConfig {
|
|
10
|
+
name: string;
|
|
11
|
+
transport?: MCPTransport;
|
|
12
|
+
|
|
13
|
+
command?: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
|
|
18
|
+
url?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
|
|
21
|
+
oauth?: MCPOAuthConfig;
|
|
22
|
+
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MCPConfig {
|
|
27
|
+
servers: MCPServerConfig[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MCPServerStatus {
|
|
31
|
+
name: string;
|
|
32
|
+
connected: boolean;
|
|
33
|
+
tools: string[];
|
|
34
|
+
error?: string;
|
|
35
|
+
transport?: MCPTransport;
|
|
36
|
+
url?: string;
|
|
37
|
+
authRequired?: boolean;
|
|
38
|
+
authenticated?: boolean;
|
|
39
|
+
}
|
|
@@ -24,14 +24,14 @@ export function buildRipgrepTool(projectRoot: string): {
|
|
|
24
24
|
.array(z.string())
|
|
25
25
|
.optional()
|
|
26
26
|
.describe('One or more glob patterns to include'),
|
|
27
|
-
maxResults: z.number().int().min(1).max(5000).optional().default(
|
|
27
|
+
maxResults: z.number().int().min(1).max(5000).optional().default(100),
|
|
28
28
|
}),
|
|
29
29
|
async execute({
|
|
30
30
|
query,
|
|
31
31
|
path = '.',
|
|
32
32
|
ignoreCase,
|
|
33
33
|
glob,
|
|
34
|
-
maxResults =
|
|
34
|
+
maxResults = 100,
|
|
35
35
|
}: {
|
|
36
36
|
query: string;
|
|
37
37
|
path?: string;
|
|
@@ -95,12 +95,20 @@ export function buildRipgrepTool(projectRoot: string): {
|
|
|
95
95
|
.split('\n')
|
|
96
96
|
.filter(Boolean)
|
|
97
97
|
.slice(0, maxResults);
|
|
98
|
+
const TEXT_MAX = 200;
|
|
98
99
|
const matches = lines.map((l) => {
|
|
99
100
|
const m = l.match(/^(.+?):(\d+):(.*)$/s);
|
|
100
|
-
if (!m)
|
|
101
|
+
if (!m)
|
|
102
|
+
return {
|
|
103
|
+
file: '',
|
|
104
|
+
line: 0,
|
|
105
|
+
text: l.length > TEXT_MAX ? l.slice(0, TEXT_MAX) + '…' : l,
|
|
106
|
+
};
|
|
101
107
|
const file = m[1];
|
|
102
108
|
const line = Number.parseInt(m[2], 10);
|
|
103
|
-
const
|
|
109
|
+
const raw = m[3];
|
|
110
|
+
const text =
|
|
111
|
+
raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + '…' : raw;
|
|
104
112
|
return { file, line, text };
|
|
105
113
|
});
|
|
106
114
|
resolve({ ok: true, count: matches.length, matches });
|
|
@@ -6,7 +6,6 @@ import { buildGitTools } from './builtin/git.ts';
|
|
|
6
6
|
import { progressUpdateTool } from './builtin/progress.ts';
|
|
7
7
|
import { buildBashTool } from './builtin/bash.ts';
|
|
8
8
|
import { buildRipgrepTool } from './builtin/ripgrep.ts';
|
|
9
|
-
import { buildGrepTool } from './builtin/grep.ts';
|
|
10
9
|
import { buildGlobTool } from './builtin/glob.ts';
|
|
11
10
|
import { buildApplyPatchTool } from './builtin/patch.ts';
|
|
12
11
|
import { buildEditTool } from './builtin/edit.ts';
|
|
@@ -16,6 +15,8 @@ import { buildWebSearchTool } from './builtin/websearch.ts';
|
|
|
16
15
|
import { buildTerminalTool } from './builtin/terminal.ts';
|
|
17
16
|
import type { TerminalManager } from '../terminals/index.ts';
|
|
18
17
|
import { initializeSkills, buildSkillTool } from '../../../skills/index.ts';
|
|
18
|
+
import { getMCPManager } from '../mcp/index.ts';
|
|
19
|
+
import { convertMCPToolsToAISDK } from '../mcp/tools.ts';
|
|
19
20
|
import fg from 'fast-glob';
|
|
20
21
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
21
22
|
import { pathToFileURL } from 'node:url';
|
|
@@ -120,8 +121,6 @@ export async function discoverProjectTools(
|
|
|
120
121
|
// Search
|
|
121
122
|
const rg = buildRipgrepTool(projectRoot);
|
|
122
123
|
tools.set(rg.name, rg.tool);
|
|
123
|
-
const grep = buildGrepTool(projectRoot);
|
|
124
|
-
tools.set(grep.name, grep.tool);
|
|
125
124
|
const glob = buildGlobTool(projectRoot);
|
|
126
125
|
tools.set(glob.name, glob.tool);
|
|
127
126
|
// Patch/apply
|
|
@@ -148,6 +147,14 @@ export async function discoverProjectTools(
|
|
|
148
147
|
const skillTool = buildSkillTool();
|
|
149
148
|
tools.set(skillTool.name, skillTool.tool);
|
|
150
149
|
|
|
150
|
+
const mcpManager = getMCPManager();
|
|
151
|
+
if (mcpManager?.started) {
|
|
152
|
+
const mcpTools = convertMCPToolsToAISDK(mcpManager);
|
|
153
|
+
for (const { name, tool } of mcpTools) {
|
|
154
|
+
tools.set(name, tool);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
async function loadFromBase(base: string | null | undefined) {
|
|
152
159
|
if (!base) return;
|
|
153
160
|
try {
|
package/src/index.ts
CHANGED
|
@@ -333,3 +333,32 @@ export {
|
|
|
333
333
|
} from './tunnel/index.ts';
|
|
334
334
|
|
|
335
335
|
export type { TunnelConnection, TunnelEvents } from './tunnel/index.ts';
|
|
336
|
+
|
|
337
|
+
// =======================
|
|
338
|
+
// MCP (Model Context Protocol)
|
|
339
|
+
// =======================
|
|
340
|
+
export {
|
|
341
|
+
MCPClientWrapper,
|
|
342
|
+
MCPServerManager,
|
|
343
|
+
convertMCPToolsToAISDK,
|
|
344
|
+
getMCPManager,
|
|
345
|
+
initializeMCP,
|
|
346
|
+
shutdownMCP,
|
|
347
|
+
loadMCPConfig,
|
|
348
|
+
addMCPServerToConfig,
|
|
349
|
+
removeMCPServerFromConfig,
|
|
350
|
+
OAuthCredentialStore,
|
|
351
|
+
OttoOAuthProvider,
|
|
352
|
+
OAuthCallbackServer,
|
|
353
|
+
} from './core/src/index.ts';
|
|
354
|
+
export type {
|
|
355
|
+
MCPServerConfig,
|
|
356
|
+
MCPConfig,
|
|
357
|
+
MCPServerStatus,
|
|
358
|
+
MCPToolInfo,
|
|
359
|
+
MCPTransport,
|
|
360
|
+
MCPOAuthConfig,
|
|
361
|
+
StoredOAuthData,
|
|
362
|
+
OttoOAuthProviderOptions,
|
|
363
|
+
CallbackResult,
|
|
364
|
+
} from './core/src/index.ts';
|