@spec2tools/core 0.1.1 → 0.1.2
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/dist/openapi-parser.js +68 -22
- package/dist/tool-executor.d.ts +2 -1
- package/dist/tool-executor.js +42 -26
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
package/dist/openapi-parser.js
CHANGED
|
@@ -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 (
|
|
241
|
-
const properties =
|
|
242
|
-
const 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
|
|
296
|
-
const
|
|
297
|
-
const combinedShape = { ...
|
|
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
|
}
|
package/dist/tool-executor.d.ts
CHANGED
|
@@ -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
|
package/dist/tool-executor.js
CHANGED
|
@@ -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.
|
|
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,
|
|
127
|
+
function separateParams(params, metadata) {
|
|
128
128
|
const pathParams = {};
|
|
129
129
|
const queryParams = {};
|
|
130
130
|
const bodyParams = {};
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 {
|