@spec2tools/core 0.1.2 → 0.2.0

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 CHANGED
@@ -64,6 +64,39 @@ const tools = createExecutableTools(toolDefs, baseUrl, authManager);
64
64
  const result = await executeToolByName(tools, 'getUser', { id: '123' });
65
65
  ```
66
66
 
67
+ ### AI SDK Tools
68
+
69
+ ```ts
70
+ import { toAISDKTools } from '@spec2tools/core';
71
+
72
+ // Convert core tools to AI SDK-compatible ToolSet
73
+ const aiTools = toAISDKTools(tools);
74
+ ```
75
+
76
+ ### Code Mode
77
+
78
+ Collapse any AI SDK `ToolSet` into 2 tools (`search` + `execute`). The model discovers endpoints via keyword search and calls them by writing Python code, executed in a sandboxed [Monty](https://github.com/pydantic/monty) interpreter.
79
+
80
+ ```ts
81
+ import { toCodeModeTools } from '@spec2tools/core';
82
+
83
+ // Convert N tools into 2 code-mode tools
84
+ const codeModeTools = toCodeModeTools(aiTools);
85
+ ```
86
+
87
+ This also works with tools from any MCP server via the AI SDK:
88
+
89
+ ```ts
90
+ import { createMCPClient } from 'ai';
91
+ import { toCodeModeTools } from '@spec2tools/core';
92
+
93
+ const client = await createMCPClient({
94
+ transport: { type: 'sse', url: 'http://localhost:3000/sse' },
95
+ });
96
+
97
+ const tools = toCodeModeTools(await client.tools());
98
+ ```
99
+
67
100
  ### Error Classes
68
101
 
69
102
  ```ts
@@ -0,0 +1,13 @@
1
+ import { generateText } from 'ai';
2
+ import type { Tool } from './types.js';
3
+ type ToolSet = NonNullable<Parameters<typeof generateText>[0]['tools']>;
4
+ /**
5
+ * Convert core Tool[] into AI SDK ToolSet.
6
+ *
7
+ * Wraps each core tool with the `ai` package's `tool()` helper,
8
+ * producing a `Record<string, CoreTool>` compatible with `generateText`,
9
+ * `streamText`, etc.
10
+ */
11
+ export declare function toAISDKTools(tools: Tool[]): ToolSet;
12
+ export type { ToolSet };
13
+ //# sourceMappingURL=ai-tools.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { tool } from 'ai';
2
+ /**
3
+ * Convert core Tool[] into AI SDK ToolSet.
4
+ *
5
+ * Wraps each core tool with the `ai` package's `tool()` helper,
6
+ * producing a `Record<string, CoreTool>` compatible with `generateText`,
7
+ * `streamText`, etc.
8
+ */
9
+ export function toAISDKTools(tools) {
10
+ const aiTools = {};
11
+ for (const t of tools) {
12
+ aiTools[t.name] = tool({
13
+ description: t.description,
14
+ inputSchema: t.parameters,
15
+ execute: async (params) => {
16
+ return t.execute(params);
17
+ },
18
+ });
19
+ }
20
+ return aiTools;
21
+ }
22
+ //# sourceMappingURL=ai-tools.js.map
@@ -0,0 +1,12 @@
1
+ import { generateText } from 'ai';
2
+ type ToolSet = NonNullable<Parameters<typeof generateText>[0]['tools']>;
3
+ /**
4
+ * Transform an AI SDK ToolSet into exactly 2 code-mode tools: `search` and `execute`.
5
+ *
6
+ * The model discovers endpoints via keyword search and calls them by writing
7
+ * Python code that is executed in a sandboxed Monty interpreter. This reduces
8
+ * token consumption by ~99.9% for large APIs (N tools → 2 tools).
9
+ */
10
+ export declare function toCodeModeTools(tools: ToolSet): ToolSet;
11
+ export {};
12
+ //# sourceMappingURL=code-mode.d.ts.map
@@ -0,0 +1,233 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { zodToJsonSchema } from 'zod-to-json-schema';
4
+ import { Monty, MontySnapshot, MontyRuntimeError, MontySyntaxError, MontyTypingError, } from '@pydantic/monty';
5
+ /**
6
+ * Convert a tool name into a valid Python identifier.
7
+ * Replaces hyphens, dots, spaces, etc. with underscores and
8
+ * prefixes with `_` if the name starts with a digit.
9
+ */
10
+ function toPythonName(name) {
11
+ let py = name.replace(/[^a-zA-Z0-9_]/g, '_');
12
+ if (/^[0-9]/.test(py))
13
+ py = `_${py}`;
14
+ return py || '_unnamed';
15
+ }
16
+ function jsonSchemaTypeToPython(schema) {
17
+ if (!schema)
18
+ return 'any';
19
+ const type = schema.type;
20
+ if (type === 'string')
21
+ return 'str';
22
+ if (type === 'integer')
23
+ return 'int';
24
+ if (type === 'number')
25
+ return 'float';
26
+ if (type === 'boolean')
27
+ return 'bool';
28
+ if (type === 'array')
29
+ return 'list';
30
+ if (type === 'object')
31
+ return 'dict';
32
+ if (schema.enum)
33
+ return 'str';
34
+ return 'any';
35
+ }
36
+ function buildIndex(tools) {
37
+ const index = [];
38
+ for (const [name, t] of Object.entries(tools)) {
39
+ const pyName = toPythonName(name);
40
+ const description = 'description' in t ? (t.description || '') : '';
41
+ const params = [];
42
+ try {
43
+ // AI SDK tool() stores the schema as inputSchema (raw Zod)
44
+ // but other wrappers may use parameters
45
+ const schema = ('inputSchema' in t ? t.inputSchema : null) ??
46
+ ('parameters' in t ? t.parameters : null);
47
+ if (schema && typeof schema === 'object') {
48
+ const schemaObj = schema;
49
+ let jsonSchema = null;
50
+ // AI SDK Schema wrapper — has a jsonSchema property
51
+ if ('jsonSchema' in schemaObj && schemaObj.jsonSchema) {
52
+ jsonSchema = schemaObj.jsonSchema;
53
+ }
54
+ // Raw Zod schema — has _def property
55
+ else if ('_def' in schemaObj) {
56
+ jsonSchema = zodToJsonSchema(schema, {
57
+ target: 'jsonSchema7',
58
+ });
59
+ }
60
+ if (jsonSchema) {
61
+ const properties = jsonSchema.properties || {};
62
+ const required = new Set(jsonSchema.required || []);
63
+ for (const [pName, pSchema] of Object.entries(properties)) {
64
+ params.push({
65
+ name: pName,
66
+ type: jsonSchemaTypeToPython(pSchema),
67
+ required: required.has(pName),
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // Cannot introspect schema; proceed with empty params
75
+ }
76
+ const paramStrs = params.map((p) => {
77
+ if (p.required)
78
+ return `${p.name}: ${p.type}`;
79
+ return `${p.name}: ${p.type} = None`;
80
+ });
81
+ const signature = `${pyName}(${paramStrs.join(', ')})`;
82
+ const exampleParams = params
83
+ .filter((p) => p.required)
84
+ .slice(0, 2)
85
+ .map((p) => {
86
+ if (p.type === 'str')
87
+ return `${p.name}="..."`;
88
+ if (p.type === 'int' || p.type === 'float')
89
+ return `${p.name}=1`;
90
+ if (p.type === 'bool')
91
+ return `${p.name}=True`;
92
+ return `${p.name}=...`;
93
+ });
94
+ const example = `${pyName}(${exampleParams.join(', ')})`;
95
+ index.push({
96
+ originalName: name,
97
+ pyName,
98
+ description,
99
+ signature,
100
+ example,
101
+ searchText: `${name} ${pyName} ${description}`.toLowerCase(),
102
+ });
103
+ }
104
+ return index;
105
+ }
106
+ /**
107
+ * Recursively convert Monty's Map objects (Python dicts) to plain JS objects
108
+ * so they JSON.stringify correctly.
109
+ */
110
+ function montyToJson(value) {
111
+ if (value instanceof Map) {
112
+ const obj = {};
113
+ for (const [k, v] of value) {
114
+ obj[String(k)] = montyToJson(v);
115
+ }
116
+ return obj;
117
+ }
118
+ if (Array.isArray(value)) {
119
+ return value.map(montyToJson);
120
+ }
121
+ return value;
122
+ }
123
+ /**
124
+ * Transform an AI SDK ToolSet into exactly 2 code-mode tools: `search` and `execute`.
125
+ *
126
+ * The model discovers endpoints via keyword search and calls them by writing
127
+ * Python code that is executed in a sandboxed Monty interpreter. This reduces
128
+ * token consumption by ~99.9% for large APIs (N tools → 2 tools).
129
+ */
130
+ export function toCodeModeTools(tools) {
131
+ const index = buildIndex(tools);
132
+ // Build a mapping from Python-safe names to original tool names
133
+ const pyToOriginal = new Map();
134
+ for (const entry of index) {
135
+ pyToOriginal.set(entry.pyName, entry.originalName);
136
+ }
137
+ const pythonNames = [...pyToOriginal.keys()];
138
+ const searchTool = tool({
139
+ description: 'Search for available API endpoints by keyword. Returns matching ' +
140
+ 'function signatures with parameter types and usage examples. ' +
141
+ 'Use this to discover what functions are available before writing code.',
142
+ inputSchema: z.object({
143
+ query: z
144
+ .string()
145
+ .describe('Keywords to search for in endpoint names and descriptions'),
146
+ }),
147
+ execute: async ({ query }) => {
148
+ const keywords = query
149
+ .toLowerCase()
150
+ .split(/\s+/)
151
+ .filter(Boolean);
152
+ const matches = index.filter((entry) => keywords.some((kw) => entry.searchText.includes(kw)));
153
+ if (matches.length === 0) {
154
+ return `No endpoints found matching "${query}". Try different keywords.`;
155
+ }
156
+ return matches
157
+ .map((m) => `${m.signature}\n ${m.description}\n Example: ${m.example}`)
158
+ .join('\n\n');
159
+ },
160
+ });
161
+ const executeTool = tool({
162
+ description: 'Execute Python code that calls API endpoints. Functions discovered ' +
163
+ 'via search are available to call directly. Always use keyword arguments.\n' +
164
+ 'Example:\n' +
165
+ ' users = listUsers(limit=10)\n' +
166
+ ' user = getUser(id="123")\n' +
167
+ ' users',
168
+ inputSchema: z.object({
169
+ code: z
170
+ .string()
171
+ .describe('Python code to execute. Use keyword arguments when calling API functions.'),
172
+ }),
173
+ execute: async ({ code }) => {
174
+ try {
175
+ const m = new Monty(code, { externalFunctions: pythonNames });
176
+ let progress = m.start({
177
+ limits: { maxDurationSecs: 30 },
178
+ });
179
+ while (progress instanceof MontySnapshot) {
180
+ const snapshot = progress;
181
+ const originalName = pyToOriginal.get(snapshot.functionName);
182
+ const toolEntry = originalName ? tools[originalName] : undefined;
183
+ if (!toolEntry ||
184
+ !('execute' in toolEntry) ||
185
+ !toolEntry.execute) {
186
+ // Should not happen since all tool names are registered,
187
+ // but handle gracefully
188
+ progress = snapshot.resume({
189
+ exception: {
190
+ type: 'ValueError',
191
+ message: `Unknown function: ${snapshot.functionName}`,
192
+ },
193
+ });
194
+ continue;
195
+ }
196
+ const params = {
197
+ ...snapshot.kwargs,
198
+ };
199
+ try {
200
+ const result = await toolEntry.execute(params, {});
201
+ progress = snapshot.resume({ returnValue: result });
202
+ }
203
+ catch (error) {
204
+ progress = snapshot.resume({
205
+ exception: {
206
+ type: 'RuntimeError',
207
+ message: error instanceof Error ? error.message : String(error),
208
+ },
209
+ });
210
+ }
211
+ }
212
+ return montyToJson(progress.output);
213
+ }
214
+ catch (error) {
215
+ if (error instanceof MontySyntaxError) {
216
+ return `Syntax Error: ${error.message}`;
217
+ }
218
+ if (error instanceof MontyRuntimeError) {
219
+ return `Runtime Error: ${error.message}\n${error.traceback()}`;
220
+ }
221
+ if (error instanceof MontyTypingError) {
222
+ return `Type Error: ${error.displayDiagnostics()}`;
223
+ }
224
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
225
+ }
226
+ },
227
+ });
228
+ return {
229
+ search: searchTool,
230
+ execute: executeTool,
231
+ };
232
+ }
233
+ //# sourceMappingURL=code-mode.js.map
package/dist/index.d.ts CHANGED
@@ -3,4 +3,7 @@ export type { HttpMethod, Tool, AuthType, AuthConfig, Session, OpenAPISpec, Path
3
3
  export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
4
4
  export { AuthManager } from './auth-manager.js';
5
5
  export { createExecutableTools, executeToolByName } from './tool-executor.js';
6
+ export { toAISDKTools } from './ai-tools.js';
7
+ export type { ToolSet } from './ai-tools.js';
8
+ export { toCodeModeTools } from './code-mode.js';
6
9
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -6,4 +6,8 @@ export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAut
6
6
  export { AuthManager } from './auth-manager.js';
7
7
  // Tool Executor
8
8
  export { createExecutableTools, executeToolByName } from './tool-executor.js';
9
+ // AI SDK Tools
10
+ export { toAISDKTools } from './ai-tools.js';
11
+ // Code Mode
12
+ export { toCodeModeTools } from './code-mode.js';
9
13
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec2tools/core",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Core utilities for OpenAPI parsing and authentication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,6 +28,8 @@
28
28
  "directory": "packages/core"
29
29
  },
30
30
  "dependencies": {
31
+ "@pydantic/monty": "^0.0.7",
32
+ "ai": "^6.0.68",
31
33
  "express": "^5.0.0",
32
34
  "open": "^10.1.0",
33
35
  "yaml": "^2.5.0",
@@ -37,7 +39,8 @@
37
39
  "devDependencies": {
38
40
  "@types/express": "^5.0.0",
39
41
  "@types/node": "^22.0.0",
40
- "typescript": "^5.6.0"
42
+ "typescript": "^5.6.0",
43
+ "vitest": "^4.0.18"
41
44
  },
42
45
  "engines": {
43
46
  "node": ">=18.0.0"
@@ -45,6 +48,8 @@
45
48
  "scripts": {
46
49
  "build": "tsc",
47
50
  "dev": "tsc --watch",
48
- "typecheck": "tsc --noEmit"
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest"
49
54
  }
50
55
  }