@ottocode/sdk 0.1.205 → 0.1.206

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.205",
3
+ "version": "0.1.206",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -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
- parts.push(
222
- `[binary data: ${(item as { mimeType?: string }).mimeType ?? 'unknown'}]`,
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
+ }
@@ -220,6 +220,7 @@ export class MCPServerManager {
220
220
 
221
221
  this.serverScopes.set(config.name, config.scope ?? 'global');
222
222
  const key = this.oauthKey(config.name);
223
+ await this.oauthStore.clearServer(key);
223
224
  const provider = new OttoOAuthProvider(key, this.oauthStore, {
224
225
  clientId: config.oauth?.clientId,
225
226
  callbackPort: config.oauth?.callbackPort,
@@ -244,6 +245,7 @@ export class MCPServerManager {
244
245
 
245
246
  if (provider.pendingAuthUrl) {
246
247
  this.pendingAuth.set(config.name, provider.pendingAuthUrl);
248
+ this.waitForAuthAndReconnect(config.name, provider);
247
249
  return provider.pendingAuthUrl;
248
250
  }
249
251
  return null;
@@ -295,6 +297,30 @@ export class MCPServerManager {
295
297
  await this.stopServer(name);
296
298
  }
297
299
 
300
+ async clearAuthData(
301
+ name: string,
302
+ scope?: 'global' | 'project',
303
+ projectRoot?: string,
304
+ ): Promise<void> {
305
+ const provider = this.authProviders.get(name);
306
+ if (provider) {
307
+ await provider.clearCredentials();
308
+ provider.cleanup();
309
+ }
310
+ this.authProviders.delete(name);
311
+ if (scope) {
312
+ this.serverScopes.set(name, scope);
313
+ }
314
+ if (projectRoot) {
315
+ this.projectRoot = projectRoot;
316
+ }
317
+ const key = this.oauthKey(name);
318
+ await this.oauthStore.clearServer(key);
319
+ if (key !== name) {
320
+ await this.oauthStore.clearServer(name);
321
+ }
322
+ }
323
+
298
324
  async getAuthStatus(
299
325
  name: string,
300
326
  ): Promise<{ authenticated: boolean; expiresAt?: number }> {
@@ -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 { convertMCPToolsToAISDK } from '../mcp/tools.ts';
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<DiscoveredTool[]> {
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
- const mcpTools = convertMCPToolsToAISDK(mcpManager);
153
- for (const { name, tool } of mcpTools) {
154
- tools.set(name, tool);
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 Array.from(tools.entries()).map(([name, tool]) => ({ name, tool }));
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';