@spec2tools/core 0.1.1 → 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
@@ -109,10 +109,39 @@ function parseSecurityScheme(scheme, scopes) {
109
109
  }
110
110
  return { type: 'none' };
111
111
  }
112
+ /**
113
+ * Resolve a $ref to its actual schema object
114
+ */
115
+ function resolveRef(ref, spec, visited = new Set()) {
116
+ // Detect circular references
117
+ if (visited.has(ref)) {
118
+ throw new UnsupportedSchemaError(ref, 'Circular $ref detected');
119
+ }
120
+ visited.add(ref);
121
+ // Only support #/components/schemas/ references
122
+ if (!ref.startsWith('#/components/schemas/')) {
123
+ throw new UnsupportedSchemaError(ref, 'Only #/components/schemas/ $refs are supported');
124
+ }
125
+ const schemaName = ref.replace('#/components/schemas/', '');
126
+ const schema = spec.components?.schemas?.[schemaName];
127
+ if (!schema) {
128
+ throw new UnsupportedSchemaError(ref, `Schema "${schemaName}" not found in components`);
129
+ }
130
+ // If the resolved schema has a $ref, resolve it recursively
131
+ if (schema.$ref) {
132
+ return resolveRef(schema.$ref, spec, visited);
133
+ }
134
+ return schema;
135
+ }
112
136
  /**
113
137
  * Convert an OpenAPI schema to a Zod schema
114
138
  */
115
- function schemaToZod(schema, path, depth = 0) {
139
+ function schemaToZod(schema, path, spec, depth = 0, visited = new Set()) {
140
+ // Resolve $ref if present
141
+ if (schema.$ref) {
142
+ const resolvedSchema = resolveRef(schema.$ref, spec, new Set(visited));
143
+ return schemaToZod(resolvedSchema, path, spec, depth, visited);
144
+ }
116
145
  // Check for unsupported features
117
146
  if (schema.anyOf) {
118
147
  throw new UnsupportedSchemaError(path, 'anyOf is not supported');
@@ -123,9 +152,6 @@ function schemaToZod(schema, path, depth = 0) {
123
152
  if (schema.allOf) {
124
153
  throw new UnsupportedSchemaError(path, 'allOf is not supported');
125
154
  }
126
- if (schema.$ref) {
127
- throw new UnsupportedSchemaError(path, '$ref is not supported');
128
- }
129
155
  // Handle array types
130
156
  if (schema.type === 'array') {
131
157
  if (!schema.items) {
@@ -134,7 +160,7 @@ function schemaToZod(schema, path, depth = 0) {
134
160
  if (schema.items.type === 'object') {
135
161
  throw new UnsupportedSchemaError(path, 'Arrays of objects are not supported');
136
162
  }
137
- const itemSchema = schemaToZod(schema.items, `${path}.items`, depth);
163
+ const itemSchema = schemaToZod(schema.items, `${path}.items`, spec, depth, visited);
138
164
  let arraySchema = z.array(itemSchema);
139
165
  if (schema.description) {
140
166
  arraySchema = arraySchema.describe(schema.description);
@@ -150,7 +176,7 @@ function schemaToZod(schema, path, depth = 0) {
150
176
  const required = schema.required || [];
151
177
  const shape = {};
152
178
  for (const [propName, propSchema] of Object.entries(properties)) {
153
- let zodProp = schemaToZod(propSchema, `${path}.${propName}`, depth + 1);
179
+ let zodProp = schemaToZod(propSchema, `${path}.${propName}`, spec, depth + 1, visited);
154
180
  if (!required.includes(propName)) {
155
181
  zodProp = zodProp.optional();
156
182
  }
@@ -197,15 +223,17 @@ function schemaToZod(schema, path, depth = 0) {
197
223
  /**
198
224
  * Build parameters schema from path and query parameters
199
225
  */
200
- function buildParametersSchema(parameters, operationId) {
226
+ function buildParametersSchema(parameters, operationId, spec) {
201
227
  const shape = {};
228
+ const pathParams = new Set();
229
+ const queryParams = new Set();
202
230
  for (const param of parameters) {
203
231
  if (param.in !== 'path' && param.in !== 'query') {
204
232
  continue; // Skip header and cookie parameters
205
233
  }
206
234
  let paramSchema;
207
235
  if (param.schema) {
208
- paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, 0);
236
+ paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, spec, 0);
209
237
  }
210
238
  else {
211
239
  paramSchema = z.string();
@@ -217,45 +245,58 @@ function buildParametersSchema(parameters, operationId) {
217
245
  paramSchema = paramSchema.optional();
218
246
  }
219
247
  shape[param.name] = paramSchema;
248
+ // Track which set this parameter belongs to
249
+ if (param.in === 'path') {
250
+ pathParams.add(param.name);
251
+ }
252
+ else if (param.in === 'query') {
253
+ queryParams.add(param.name);
254
+ }
220
255
  }
221
- return shape;
256
+ return { shape, pathParams, queryParams };
222
257
  }
223
258
  /**
224
259
  * Build request body schema
225
260
  */
226
- function buildRequestBodySchema(operation, operationId) {
261
+ function buildRequestBodySchema(operation, operationId, spec) {
262
+ const shape = {};
263
+ const bodyParams = new Set();
227
264
  if (!operation.requestBody?.content) {
228
- return {};
265
+ return { shape, bodyParams };
229
266
  }
230
267
  const jsonContent = operation.requestBody.content['application/json'];
231
268
  if (!jsonContent?.schema) {
232
- return {};
269
+ return { shape, bodyParams };
233
270
  }
234
271
  const schema = jsonContent.schema;
235
272
  // Check for file uploads
236
273
  if (schema.type === 'string' && schema.format === 'binary') {
237
274
  throw new UnsupportedSchemaError(`${operationId}.requestBody`, 'File uploads are not supported');
238
275
  }
276
+ // Resolve $ref if present
277
+ let resolvedSchema = schema;
278
+ if (schema.$ref) {
279
+ resolvedSchema = resolveRef(schema.$ref, spec);
280
+ }
239
281
  // For object schemas, flatten properties into the parameter shape
240
- if (schema.type === 'object' || schema.properties) {
241
- const properties = schema.properties || {};
242
- const required = schema.required || [];
243
- const shape = {};
282
+ if (resolvedSchema.type === 'object' || resolvedSchema.properties) {
283
+ const properties = resolvedSchema.properties || {};
284
+ const required = resolvedSchema.required || [];
244
285
  for (const [propName, propSchema] of Object.entries(properties)) {
245
286
  // Check for file upload in properties
246
287
  if (propSchema.type === 'string' &&
247
288
  propSchema.format === 'binary') {
248
289
  throw new UnsupportedSchemaError(`${operationId}.requestBody.${propName}`, 'File uploads are not supported');
249
290
  }
250
- let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, 1);
291
+ let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, spec, 1);
251
292
  if (!required.includes(propName)) {
252
293
  zodProp = zodProp.optional();
253
294
  }
254
295
  shape[propName] = zodProp;
296
+ bodyParams.add(propName); // Track that this is a body parameter
255
297
  }
256
- return shape;
257
298
  }
258
- return {};
299
+ return { shape, bodyParams };
259
300
  }
260
301
  /**
261
302
  * Generate tool name from operation
@@ -292,9 +333,9 @@ export function parseOperations(spec) {
292
333
  ...(operation.parameters || []),
293
334
  ];
294
335
  // Build combined schema from parameters and request body
295
- const parameterShape = buildParametersSchema(allParameters, operationId);
296
- const bodyShape = buildRequestBodySchema(operation, operationId);
297
- const combinedShape = { ...parameterShape, ...bodyShape };
336
+ const parametersResult = buildParametersSchema(allParameters, operationId, spec);
337
+ const bodyResult = buildRequestBodySchema(operation, operationId, spec);
338
+ const combinedShape = { ...parametersResult.shape, ...bodyResult.shape };
298
339
  const parameters = z.object(combinedShape);
299
340
  // Extract operation-specific auth config
300
341
  const authConfig = extractOperationAuthConfig(spec, operation);
@@ -305,6 +346,11 @@ export function parseOperations(spec) {
305
346
  httpMethod: method,
306
347
  path,
307
348
  authConfig,
349
+ parameterMetadata: {
350
+ pathParams: parametersResult.pathParams,
351
+ queryParams: parametersResult.queryParams,
352
+ bodyParams: bodyResult.bodyParams,
353
+ },
308
354
  });
309
355
  }
310
356
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Tool, HttpMethod, AuthConfig } from './types.js';
2
+ import { Tool, HttpMethod, AuthConfig, ParameterMetadata } from './types.js';
3
3
  import { AuthManager } from './auth-manager.js';
4
4
  interface ToolDefinition {
5
5
  name: string;
@@ -8,6 +8,7 @@ interface ToolDefinition {
8
8
  httpMethod: HttpMethod;
9
9
  path: string;
10
10
  authConfig?: AuthConfig;
11
+ parameterMetadata?: ParameterMetadata;
11
12
  }
12
13
  /**
13
14
  * Create executable tools from tool definitions
@@ -19,8 +19,8 @@ function createExecutor(tool, baseUrl, authManager) {
19
19
  const validatedParams = tool.parameters.parse(params);
20
20
  // Build URL with path parameters replaced
21
21
  let url = buildUrl(baseUrl, tool.path, validatedParams);
22
- // Separate path, query, and body parameters
23
- const { queryParams, bodyParams } = separateParams(validatedParams, tool.path);
22
+ // Separate path, query, and body parameters using metadata
23
+ const { queryParams, bodyParams } = separateParams(validatedParams, tool.parameterMetadata);
24
24
  // Add query parameters
25
25
  const urlObj = new URL(url);
26
26
  for (const [key, value] of Object.entries(queryParams)) {
@@ -122,39 +122,55 @@ function buildUrl(baseUrl, path, params) {
122
122
  return `${baseUrl}${finalPath}`;
123
123
  }
124
124
  /**
125
- * Separate parameters into path, query, and body params
125
+ * Separate parameters into path, query, and body params using metadata
126
126
  */
127
- function separateParams(params, path) {
127
+ function separateParams(params, metadata) {
128
128
  const pathParams = {};
129
129
  const queryParams = {};
130
130
  const bodyParams = {};
131
- // Extract path parameter names
132
- const pathParamNames = new Set();
133
- const pathParamRegex = /\{(\w+)\}/g;
134
- let match;
135
- while ((match = pathParamRegex.exec(path)) !== null) {
136
- pathParamNames.add(match[1]);
137
- }
138
- for (const [key, value] of Object.entries(params)) {
139
- if (pathParamNames.has(key)) {
140
- pathParams[key] = value;
141
- }
142
- else if (isPrimitive(value)) {
143
- // Primitive values go to query params
144
- queryParams[key] = value;
131
+ // If we have metadata, use it to categorize parameters
132
+ if (metadata) {
133
+ for (const [key, value] of Object.entries(params)) {
134
+ if (metadata.pathParams.has(key)) {
135
+ pathParams[key] = value;
136
+ }
137
+ else if (metadata.queryParams.has(key)) {
138
+ queryParams[key] = value;
139
+ }
140
+ else if (metadata.bodyParams.has(key)) {
141
+ bodyParams[key] = value;
142
+ }
143
+ else {
144
+ // Fallback: if not in metadata, treat as body param
145
+ bodyParams[key] = value;
146
+ }
145
147
  }
146
- else {
147
- // Complex values go to body
148
- bodyParams[key] = value;
148
+ }
149
+ else {
150
+ // Fallback to old behavior if no metadata (shouldn't happen with new parser)
151
+ // This keeps backward compatibility
152
+ const pathParamNames = extractPathParamNames(params);
153
+ for (const [key, value] of Object.entries(params)) {
154
+ if (pathParamNames.has(key)) {
155
+ pathParams[key] = value;
156
+ }
157
+ else if (isPrimitive(value)) {
158
+ queryParams[key] = value;
159
+ }
160
+ else {
161
+ bodyParams[key] = value;
162
+ }
149
163
  }
150
164
  }
151
- // For simplicity, if there are non-path primitive params,
152
- // we need to determine if they're query or body based on HTTP method
153
- // Since we don't have that info here, we'll treat all non-path primitives
154
- // as potential query params for GET/DELETE, and body params for POST/PUT/PATCH
155
- // This is handled by the executor which knows the method
156
165
  return { pathParams, queryParams, bodyParams };
157
166
  }
167
+ /**
168
+ * Extract path parameter names from params (fallback)
169
+ */
170
+ function extractPathParamNames(params) {
171
+ // This is a fallback - in practice, metadata should always be provided
172
+ return new Set();
173
+ }
158
174
  /**
159
175
  * Check if value is a primitive type
160
176
  */
package/dist/types.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3
+ export interface ParameterMetadata {
4
+ /** Parameters that come from path (e.g., {id} in /users/{id}) */
5
+ pathParams: Set<string>;
6
+ /** Parameters that come from query string */
7
+ queryParams: Set<string>;
8
+ /** Parameters that come from request body */
9
+ bodyParams: Set<string>;
10
+ }
3
11
  export interface Tool {
4
12
  name: string;
5
13
  description: string;
@@ -8,6 +16,7 @@ export interface Tool {
8
16
  httpMethod: HttpMethod;
9
17
  path: string;
10
18
  authConfig?: AuthConfig;
19
+ parameterMetadata?: ParameterMetadata;
11
20
  }
12
21
  export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'basic' | 'none';
13
22
  export interface AuthConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec2tools/core",
3
- "version": "0.1.1",
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
  }