@spec2tools/core 0.1.0 → 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/auth-manager.d.ts +13 -6
- package/dist/auth-manager.js +71 -35
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/openapi-parser.d.ts +6 -1
- package/dist/openapi-parser.js +98 -26
- package/dist/tool-executor.d.ts +3 -1
- package/dist/tool-executor.js +74 -31
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
package/dist/auth-manager.d.ts
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import { AuthConfig } from './types.js';
|
|
2
2
|
export declare class AuthManager {
|
|
3
|
-
private
|
|
3
|
+
private globalAuthConfig;
|
|
4
4
|
private accessToken?;
|
|
5
5
|
private refreshToken?;
|
|
6
6
|
private clientId?;
|
|
7
7
|
private clientSecret?;
|
|
8
8
|
private codeVerifier?;
|
|
9
|
-
constructor(
|
|
9
|
+
constructor(globalAuthConfig: AuthConfig);
|
|
10
10
|
/**
|
|
11
11
|
* Check if authentication is required
|
|
12
12
|
*/
|
|
13
|
-
requiresAuth(): boolean;
|
|
13
|
+
requiresAuth(authConfig?: AuthConfig): boolean;
|
|
14
14
|
/**
|
|
15
15
|
* Get the current access token
|
|
16
16
|
*/
|
|
17
17
|
getAccessToken(): string | undefined;
|
|
18
18
|
/**
|
|
19
19
|
* Get authorization headers for requests
|
|
20
|
+
* @param authConfig Optional tool-specific auth config that overrides the global config
|
|
20
21
|
*/
|
|
21
|
-
getAuthHeaders(): Record<string, string>;
|
|
22
|
+
getAuthHeaders(authConfig?: AuthConfig): Record<string, string>;
|
|
22
23
|
/**
|
|
23
24
|
* Get query parameters for API key auth
|
|
25
|
+
* @param authConfig Optional tool-specific auth config that overrides the global config
|
|
24
26
|
*/
|
|
25
|
-
getAuthQueryParams(): Record<string, string>;
|
|
27
|
+
getAuthQueryParams(authConfig?: AuthConfig): Record<string, string>;
|
|
26
28
|
/**
|
|
27
29
|
* Perform authentication based on config type
|
|
30
|
+
* @param authConfig Optional tool-specific auth config that overrides the global config
|
|
28
31
|
*/
|
|
29
|
-
authenticate(): Promise<void>;
|
|
32
|
+
authenticate(authConfig?: AuthConfig): Promise<void>;
|
|
30
33
|
/**
|
|
31
34
|
* Prompt user for API key
|
|
32
35
|
*/
|
|
@@ -35,6 +38,10 @@ export declare class AuthManager {
|
|
|
35
38
|
* Prompt user for bearer token
|
|
36
39
|
*/
|
|
37
40
|
private promptForBearerToken;
|
|
41
|
+
/**
|
|
42
|
+
* Prompt user for basic auth credentials
|
|
43
|
+
*/
|
|
44
|
+
private promptForBasicAuth;
|
|
38
45
|
/**
|
|
39
46
|
* Perform OAuth2 authorization code flow
|
|
40
47
|
*/
|
package/dist/auth-manager.js
CHANGED
|
@@ -7,20 +7,21 @@ import * as readline from 'readline';
|
|
|
7
7
|
const CALLBACK_PORT = 54321;
|
|
8
8
|
const CALLBACK_PATH = '/callback';
|
|
9
9
|
export class AuthManager {
|
|
10
|
-
|
|
10
|
+
globalAuthConfig;
|
|
11
11
|
accessToken;
|
|
12
12
|
refreshToken;
|
|
13
13
|
clientId;
|
|
14
14
|
clientSecret;
|
|
15
15
|
codeVerifier;
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
16
|
+
constructor(globalAuthConfig) {
|
|
17
|
+
this.globalAuthConfig = globalAuthConfig;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Check if authentication is required
|
|
21
21
|
*/
|
|
22
|
-
requiresAuth() {
|
|
23
|
-
|
|
22
|
+
requiresAuth(authConfig) {
|
|
23
|
+
const config = authConfig ?? this.globalAuthConfig;
|
|
24
|
+
return config.type !== 'none';
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Get the current access token
|
|
@@ -30,18 +31,22 @@ export class AuthManager {
|
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Get authorization headers for requests
|
|
34
|
+
* @param authConfig Optional tool-specific auth config that overrides the global config
|
|
33
35
|
*/
|
|
34
|
-
getAuthHeaders() {
|
|
36
|
+
getAuthHeaders(authConfig) {
|
|
35
37
|
if (!this.accessToken) {
|
|
36
38
|
return {};
|
|
37
39
|
}
|
|
38
|
-
|
|
40
|
+
const config = authConfig ?? this.globalAuthConfig;
|
|
41
|
+
switch (config.type) {
|
|
39
42
|
case 'oauth2':
|
|
40
43
|
case 'bearer':
|
|
41
44
|
return { Authorization: `Bearer ${this.accessToken}` };
|
|
45
|
+
case 'basic':
|
|
46
|
+
return { Authorization: `Basic ${this.accessToken}` };
|
|
42
47
|
case 'apiKey':
|
|
43
|
-
if (
|
|
44
|
-
return { [
|
|
48
|
+
if (config.apiKeyIn === 'header' && config.apiKeyHeader) {
|
|
49
|
+
return { [config.apiKeyHeader]: this.accessToken };
|
|
45
50
|
}
|
|
46
51
|
return {};
|
|
47
52
|
default:
|
|
@@ -50,30 +55,37 @@ export class AuthManager {
|
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* Get query parameters for API key auth
|
|
58
|
+
* @param authConfig Optional tool-specific auth config that overrides the global config
|
|
53
59
|
*/
|
|
54
|
-
getAuthQueryParams() {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
getAuthQueryParams(authConfig) {
|
|
61
|
+
const config = authConfig ?? this.globalAuthConfig;
|
|
62
|
+
if (config.type === 'apiKey' &&
|
|
63
|
+
config.apiKeyIn === 'query' &&
|
|
64
|
+
config.apiKeyHeader &&
|
|
58
65
|
this.accessToken) {
|
|
59
|
-
return { [
|
|
66
|
+
return { [config.apiKeyHeader]: this.accessToken };
|
|
60
67
|
}
|
|
61
68
|
return {};
|
|
62
69
|
}
|
|
63
70
|
/**
|
|
64
71
|
* Perform authentication based on config type
|
|
72
|
+
* @param authConfig Optional tool-specific auth config that overrides the global config
|
|
65
73
|
*/
|
|
66
|
-
async authenticate() {
|
|
67
|
-
|
|
74
|
+
async authenticate(authConfig) {
|
|
75
|
+
const config = authConfig ?? this.globalAuthConfig;
|
|
76
|
+
switch (config.type) {
|
|
68
77
|
case 'oauth2':
|
|
69
|
-
await this.performOAuth2Flow();
|
|
78
|
+
await this.performOAuth2Flow(config);
|
|
70
79
|
break;
|
|
71
80
|
case 'apiKey':
|
|
72
|
-
await this.promptForApiKey();
|
|
81
|
+
await this.promptForApiKey(config);
|
|
73
82
|
break;
|
|
74
83
|
case 'bearer':
|
|
75
84
|
await this.promptForBearerToken();
|
|
76
85
|
break;
|
|
86
|
+
case 'basic':
|
|
87
|
+
await this.promptForBasicAuth();
|
|
88
|
+
break;
|
|
77
89
|
case 'none':
|
|
78
90
|
// No authentication needed
|
|
79
91
|
break;
|
|
@@ -82,12 +94,12 @@ export class AuthManager {
|
|
|
82
94
|
/**
|
|
83
95
|
* Prompt user for API key
|
|
84
96
|
*/
|
|
85
|
-
async promptForApiKey() {
|
|
97
|
+
async promptForApiKey(config) {
|
|
86
98
|
const rl = readline.createInterface({
|
|
87
99
|
input: process.stdin,
|
|
88
100
|
output: process.stdout,
|
|
89
101
|
});
|
|
90
|
-
const headerName =
|
|
102
|
+
const headerName = config.apiKeyHeader || 'API-Key';
|
|
91
103
|
this.accessToken = await new Promise((resolve) => {
|
|
92
104
|
rl.question(`Enter your API key (${headerName}): `, (answer) => {
|
|
93
105
|
rl.close();
|
|
@@ -116,26 +128,50 @@ export class AuthManager {
|
|
|
116
128
|
throw new AuthenticationError('Bearer token is required');
|
|
117
129
|
}
|
|
118
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Prompt user for basic auth credentials
|
|
133
|
+
*/
|
|
134
|
+
async promptForBasicAuth() {
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout,
|
|
138
|
+
});
|
|
139
|
+
const question = (prompt) => {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
rl.question(prompt, (answer) => {
|
|
142
|
+
resolve(answer.trim());
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
const username = await question('Enter username: ');
|
|
147
|
+
const password = await question('Enter password: ');
|
|
148
|
+
rl.close();
|
|
149
|
+
if (!username || !password) {
|
|
150
|
+
throw new AuthenticationError('Username and password are required for Basic auth');
|
|
151
|
+
}
|
|
152
|
+
// Base64 encode the credentials
|
|
153
|
+
this.accessToken = Buffer.from(`${username}:${password}`).toString('base64');
|
|
154
|
+
}
|
|
119
155
|
/**
|
|
120
156
|
* Perform OAuth2 authorization code flow
|
|
121
157
|
*/
|
|
122
|
-
async performOAuth2Flow() {
|
|
123
|
-
if (!
|
|
158
|
+
async performOAuth2Flow(config) {
|
|
159
|
+
if (!config.authorizationUrl || !config.tokenUrl) {
|
|
124
160
|
throw new AuthenticationError('OAuth2 requires authorizationUrl and tokenUrl');
|
|
125
161
|
}
|
|
126
162
|
// Register client dynamically
|
|
127
|
-
await this.registerClient();
|
|
163
|
+
await this.registerClient(config);
|
|
128
164
|
// Start local callback server
|
|
129
|
-
const authCode = await this.startCallbackServerAndAuthorize();
|
|
165
|
+
const authCode = await this.startCallbackServerAndAuthorize(config);
|
|
130
166
|
// Exchange code for token
|
|
131
|
-
await this.exchangeCodeForToken(authCode);
|
|
167
|
+
await this.exchangeCodeForToken(authCode, config);
|
|
132
168
|
}
|
|
133
169
|
/**
|
|
134
170
|
* Register OAuth2 client dynamically
|
|
135
171
|
*/
|
|
136
|
-
async registerClient() {
|
|
172
|
+
async registerClient(config) {
|
|
137
173
|
// Derive registration URL from token URL (replace /token with /register)
|
|
138
|
-
const registrationUrl =
|
|
174
|
+
const registrationUrl = config.tokenUrl.replace(/\/token$/, '/register');
|
|
139
175
|
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
140
176
|
console.log('Registering OAuth2 client...');
|
|
141
177
|
const response = await fetch(registrationUrl, {
|
|
@@ -162,7 +198,7 @@ export class AuthManager {
|
|
|
162
198
|
/**
|
|
163
199
|
* Start local callback server and initiate authorization
|
|
164
200
|
*/
|
|
165
|
-
async startCallbackServerAndAuthorize() {
|
|
201
|
+
async startCallbackServerAndAuthorize(config) {
|
|
166
202
|
return new Promise((resolve, reject) => {
|
|
167
203
|
const app = express();
|
|
168
204
|
let server;
|
|
@@ -211,7 +247,7 @@ export class AuthManager {
|
|
|
211
247
|
server = createServer(app);
|
|
212
248
|
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
|
|
213
249
|
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
214
|
-
const authUrl = this.buildAuthorizationUrl(redirectUri);
|
|
250
|
+
const authUrl = this.buildAuthorizationUrl(redirectUri, config);
|
|
215
251
|
console.log('\nAuthentication required. Opening browser...');
|
|
216
252
|
console.log(`If browser doesn't open, visit: ${authUrl}\n`);
|
|
217
253
|
open(authUrl).catch(() => {
|
|
@@ -249,8 +285,8 @@ export class AuthManager {
|
|
|
249
285
|
/**
|
|
250
286
|
* Build the OAuth2 authorization URL
|
|
251
287
|
*/
|
|
252
|
-
buildAuthorizationUrl(redirectUri) {
|
|
253
|
-
const url = new URL(
|
|
288
|
+
buildAuthorizationUrl(redirectUri, config) {
|
|
289
|
+
const url = new URL(config.authorizationUrl);
|
|
254
290
|
// Generate PKCE
|
|
255
291
|
const pkce = this.generatePKCE();
|
|
256
292
|
this.codeVerifier = pkce.verifier;
|
|
@@ -259,8 +295,8 @@ export class AuthManager {
|
|
|
259
295
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
260
296
|
url.searchParams.set('code_challenge', pkce.challenge);
|
|
261
297
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
262
|
-
if (
|
|
263
|
-
url.searchParams.set('scope',
|
|
298
|
+
if (config.scopes && config.scopes.length > 0) {
|
|
299
|
+
url.searchParams.set('scope', config.scopes.join(' '));
|
|
264
300
|
}
|
|
265
301
|
// Generate state for CSRF protection
|
|
266
302
|
const state = crypto.randomBytes(16).toString('base64url');
|
|
@@ -270,7 +306,7 @@ export class AuthManager {
|
|
|
270
306
|
/**
|
|
271
307
|
* Exchange authorization code for access token
|
|
272
308
|
*/
|
|
273
|
-
async exchangeCodeForToken(code) {
|
|
309
|
+
async exchangeCodeForToken(code, config) {
|
|
274
310
|
const redirectUri = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
275
311
|
const params = new URLSearchParams({
|
|
276
312
|
grant_type: 'authorization_code',
|
|
@@ -283,7 +319,7 @@ export class AuthManager {
|
|
|
283
319
|
if (this.clientSecret) {
|
|
284
320
|
params.set('client_secret', this.clientSecret);
|
|
285
321
|
}
|
|
286
|
-
const response = await fetch(
|
|
322
|
+
const response = await fetch(config.tokenUrl, {
|
|
287
323
|
method: 'POST',
|
|
288
324
|
headers: {
|
|
289
325
|
'Content-Type': 'application/x-www-form-urlencoded',
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
|
|
2
2
|
export type { HttpMethod, Tool, AuthType, AuthConfig, Session, OpenAPISpec, PathItem, Operation, Parameter, RequestBody, MediaType, Response, SchemaObject, SecurityScheme, } from './types.js';
|
|
3
|
-
export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
|
|
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
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Error classes
|
|
2
2
|
export { UnsupportedSchemaError, AuthenticationError, ToolExecutionError, SpecLoadError, } from './errors.js';
|
|
3
3
|
// OpenAPI Parser
|
|
4
|
-
export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
|
|
4
|
+
export { loadOpenAPISpec, extractBaseUrl, extractAuthConfig, extractOperationAuthConfig, parseOperations, formatToolSchema, formatToolSignature, } from './openapi-parser.js';
|
|
5
5
|
// Auth Manager
|
|
6
6
|
export { AuthManager } from './auth-manager.js';
|
|
7
7
|
// Tool Executor
|
package/dist/openapi-parser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpenAPISpec, Tool, AuthConfig } from './types.js';
|
|
1
|
+
import { OpenAPISpec, Operation, Tool, AuthConfig } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fetch and parse an OpenAPI specification from URL or file path
|
|
4
4
|
*/
|
|
@@ -11,6 +11,11 @@ export declare function extractBaseUrl(spec: OpenAPISpec): string;
|
|
|
11
11
|
* Extract authentication configuration from security schemes
|
|
12
12
|
*/
|
|
13
13
|
export declare function extractAuthConfig(spec: OpenAPISpec): AuthConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Extract authentication configuration for a specific operation
|
|
16
|
+
* Uses operation-level security if defined, otherwise falls back to global security
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractOperationAuthConfig(spec: OpenAPISpec, operation: Operation): AuthConfig;
|
|
14
19
|
/**
|
|
15
20
|
* Parse all operations from the OpenAPI spec and generate tool definitions
|
|
16
21
|
*/
|
package/dist/openapi-parser.js
CHANGED
|
@@ -47,13 +47,33 @@ export function extractBaseUrl(spec) {
|
|
|
47
47
|
* Extract authentication configuration from security schemes
|
|
48
48
|
*/
|
|
49
49
|
export function extractAuthConfig(spec) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
return extractAuthConfigFromSecurity(spec.security, spec.components?.securitySchemes);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract authentication configuration for a specific operation
|
|
54
|
+
* Uses operation-level security if defined, otherwise falls back to global security
|
|
55
|
+
*/
|
|
56
|
+
export function extractOperationAuthConfig(spec, operation) {
|
|
57
|
+
// If operation has its own security field, use it (even if empty array = no auth)
|
|
58
|
+
if (operation.security !== undefined) {
|
|
59
|
+
return extractAuthConfigFromSecurity(operation.security, spec.components?.securitySchemes);
|
|
60
|
+
}
|
|
61
|
+
// Fall back to global security
|
|
62
|
+
return extractAuthConfig(spec);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract auth config from a security requirement array
|
|
66
|
+
*/
|
|
67
|
+
function extractAuthConfigFromSecurity(security, securitySchemes) {
|
|
68
|
+
// Empty security array means explicitly no auth required
|
|
69
|
+
if (security !== undefined && security.length === 0) {
|
|
70
|
+
return { type: 'none' };
|
|
71
|
+
}
|
|
72
|
+
if (!securitySchemes || !security || security.length === 0) {
|
|
53
73
|
return { type: 'none' };
|
|
54
74
|
}
|
|
55
75
|
// Get the first security requirement
|
|
56
|
-
const securityReq =
|
|
76
|
+
const securityReq = security[0];
|
|
57
77
|
const schemeName = Object.keys(securityReq)[0];
|
|
58
78
|
const scheme = securitySchemes[schemeName];
|
|
59
79
|
if (!scheme) {
|
|
@@ -84,12 +104,44 @@ function parseSecurityScheme(scheme, scopes) {
|
|
|
84
104
|
if (scheme.type === 'http' && scheme.scheme === 'bearer') {
|
|
85
105
|
return { type: 'bearer' };
|
|
86
106
|
}
|
|
107
|
+
if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
|
108
|
+
return { type: 'basic' };
|
|
109
|
+
}
|
|
87
110
|
return { type: 'none' };
|
|
88
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
|
+
}
|
|
89
136
|
/**
|
|
90
137
|
* Convert an OpenAPI schema to a Zod schema
|
|
91
138
|
*/
|
|
92
|
-
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
|
+
}
|
|
93
145
|
// Check for unsupported features
|
|
94
146
|
if (schema.anyOf) {
|
|
95
147
|
throw new UnsupportedSchemaError(path, 'anyOf is not supported');
|
|
@@ -100,9 +152,6 @@ function schemaToZod(schema, path, depth = 0) {
|
|
|
100
152
|
if (schema.allOf) {
|
|
101
153
|
throw new UnsupportedSchemaError(path, 'allOf is not supported');
|
|
102
154
|
}
|
|
103
|
-
if (schema.$ref) {
|
|
104
|
-
throw new UnsupportedSchemaError(path, '$ref is not supported');
|
|
105
|
-
}
|
|
106
155
|
// Handle array types
|
|
107
156
|
if (schema.type === 'array') {
|
|
108
157
|
if (!schema.items) {
|
|
@@ -111,7 +160,7 @@ function schemaToZod(schema, path, depth = 0) {
|
|
|
111
160
|
if (schema.items.type === 'object') {
|
|
112
161
|
throw new UnsupportedSchemaError(path, 'Arrays of objects are not supported');
|
|
113
162
|
}
|
|
114
|
-
const itemSchema = schemaToZod(schema.items, `${path}.items`, depth);
|
|
163
|
+
const itemSchema = schemaToZod(schema.items, `${path}.items`, spec, depth, visited);
|
|
115
164
|
let arraySchema = z.array(itemSchema);
|
|
116
165
|
if (schema.description) {
|
|
117
166
|
arraySchema = arraySchema.describe(schema.description);
|
|
@@ -127,7 +176,7 @@ function schemaToZod(schema, path, depth = 0) {
|
|
|
127
176
|
const required = schema.required || [];
|
|
128
177
|
const shape = {};
|
|
129
178
|
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
130
|
-
let zodProp = schemaToZod(propSchema, `${path}.${propName}`, depth + 1);
|
|
179
|
+
let zodProp = schemaToZod(propSchema, `${path}.${propName}`, spec, depth + 1, visited);
|
|
131
180
|
if (!required.includes(propName)) {
|
|
132
181
|
zodProp = zodProp.optional();
|
|
133
182
|
}
|
|
@@ -174,15 +223,17 @@ function schemaToZod(schema, path, depth = 0) {
|
|
|
174
223
|
/**
|
|
175
224
|
* Build parameters schema from path and query parameters
|
|
176
225
|
*/
|
|
177
|
-
function buildParametersSchema(parameters, operationId) {
|
|
226
|
+
function buildParametersSchema(parameters, operationId, spec) {
|
|
178
227
|
const shape = {};
|
|
228
|
+
const pathParams = new Set();
|
|
229
|
+
const queryParams = new Set();
|
|
179
230
|
for (const param of parameters) {
|
|
180
231
|
if (param.in !== 'path' && param.in !== 'query') {
|
|
181
232
|
continue; // Skip header and cookie parameters
|
|
182
233
|
}
|
|
183
234
|
let paramSchema;
|
|
184
235
|
if (param.schema) {
|
|
185
|
-
paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, 0);
|
|
236
|
+
paramSchema = schemaToZod(param.schema, `${operationId}.parameters.${param.name}`, spec, 0);
|
|
186
237
|
}
|
|
187
238
|
else {
|
|
188
239
|
paramSchema = z.string();
|
|
@@ -194,45 +245,58 @@ function buildParametersSchema(parameters, operationId) {
|
|
|
194
245
|
paramSchema = paramSchema.optional();
|
|
195
246
|
}
|
|
196
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
|
+
}
|
|
197
255
|
}
|
|
198
|
-
return shape;
|
|
256
|
+
return { shape, pathParams, queryParams };
|
|
199
257
|
}
|
|
200
258
|
/**
|
|
201
259
|
* Build request body schema
|
|
202
260
|
*/
|
|
203
|
-
function buildRequestBodySchema(operation, operationId) {
|
|
261
|
+
function buildRequestBodySchema(operation, operationId, spec) {
|
|
262
|
+
const shape = {};
|
|
263
|
+
const bodyParams = new Set();
|
|
204
264
|
if (!operation.requestBody?.content) {
|
|
205
|
-
return {};
|
|
265
|
+
return { shape, bodyParams };
|
|
206
266
|
}
|
|
207
267
|
const jsonContent = operation.requestBody.content['application/json'];
|
|
208
268
|
if (!jsonContent?.schema) {
|
|
209
|
-
return {};
|
|
269
|
+
return { shape, bodyParams };
|
|
210
270
|
}
|
|
211
271
|
const schema = jsonContent.schema;
|
|
212
272
|
// Check for file uploads
|
|
213
273
|
if (schema.type === 'string' && schema.format === 'binary') {
|
|
214
274
|
throw new UnsupportedSchemaError(`${operationId}.requestBody`, 'File uploads are not supported');
|
|
215
275
|
}
|
|
276
|
+
// Resolve $ref if present
|
|
277
|
+
let resolvedSchema = schema;
|
|
278
|
+
if (schema.$ref) {
|
|
279
|
+
resolvedSchema = resolveRef(schema.$ref, spec);
|
|
280
|
+
}
|
|
216
281
|
// For object schemas, flatten properties into the parameter shape
|
|
217
|
-
if (
|
|
218
|
-
const properties =
|
|
219
|
-
const required =
|
|
220
|
-
const shape = {};
|
|
282
|
+
if (resolvedSchema.type === 'object' || resolvedSchema.properties) {
|
|
283
|
+
const properties = resolvedSchema.properties || {};
|
|
284
|
+
const required = resolvedSchema.required || [];
|
|
221
285
|
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
222
286
|
// Check for file upload in properties
|
|
223
287
|
if (propSchema.type === 'string' &&
|
|
224
288
|
propSchema.format === 'binary') {
|
|
225
289
|
throw new UnsupportedSchemaError(`${operationId}.requestBody.${propName}`, 'File uploads are not supported');
|
|
226
290
|
}
|
|
227
|
-
let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, 1);
|
|
291
|
+
let zodProp = schemaToZod(propSchema, `${operationId}.requestBody.${propName}`, spec, 1);
|
|
228
292
|
if (!required.includes(propName)) {
|
|
229
293
|
zodProp = zodProp.optional();
|
|
230
294
|
}
|
|
231
295
|
shape[propName] = zodProp;
|
|
296
|
+
bodyParams.add(propName); // Track that this is a body parameter
|
|
232
297
|
}
|
|
233
|
-
return shape;
|
|
234
298
|
}
|
|
235
|
-
return {};
|
|
299
|
+
return { shape, bodyParams };
|
|
236
300
|
}
|
|
237
301
|
/**
|
|
238
302
|
* Generate tool name from operation
|
|
@@ -269,16 +333,24 @@ export function parseOperations(spec) {
|
|
|
269
333
|
...(operation.parameters || []),
|
|
270
334
|
];
|
|
271
335
|
// Build combined schema from parameters and request body
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const combinedShape = { ...
|
|
336
|
+
const parametersResult = buildParametersSchema(allParameters, operationId, spec);
|
|
337
|
+
const bodyResult = buildRequestBodySchema(operation, operationId, spec);
|
|
338
|
+
const combinedShape = { ...parametersResult.shape, ...bodyResult.shape };
|
|
275
339
|
const parameters = z.object(combinedShape);
|
|
340
|
+
// Extract operation-specific auth config
|
|
341
|
+
const authConfig = extractOperationAuthConfig(spec, operation);
|
|
276
342
|
tools.push({
|
|
277
343
|
name: operationId,
|
|
278
344
|
description: operation.summary || operation.description || `${method} ${path}`,
|
|
279
345
|
parameters,
|
|
280
346
|
httpMethod: method,
|
|
281
347
|
path,
|
|
348
|
+
authConfig,
|
|
349
|
+
parameterMetadata: {
|
|
350
|
+
pathParams: parametersResult.pathParams,
|
|
351
|
+
queryParams: parametersResult.queryParams,
|
|
352
|
+
bodyParams: bodyResult.bodyParams,
|
|
353
|
+
},
|
|
282
354
|
});
|
|
283
355
|
}
|
|
284
356
|
}
|
package/dist/tool-executor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { Tool, HttpMethod } 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;
|
|
@@ -7,6 +7,8 @@ interface ToolDefinition {
|
|
|
7
7
|
parameters: z.ZodObject<z.ZodRawShape>;
|
|
8
8
|
httpMethod: HttpMethod;
|
|
9
9
|
path: string;
|
|
10
|
+
authConfig?: AuthConfig;
|
|
11
|
+
parameterMetadata?: ParameterMetadata;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
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)) {
|
|
@@ -28,15 +28,20 @@ function createExecutor(tool, baseUrl, authManager) {
|
|
|
28
28
|
urlObj.searchParams.set(key, String(value));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// Check if this tool requires auth (respects operation-level security)
|
|
32
|
+
// Tool-level authConfig overrides the global authConfig
|
|
33
|
+
const toolRequiresAuth = authManager.requiresAuth(tool.authConfig);
|
|
31
34
|
// Add auth query params if needed
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
if (toolRequiresAuth) {
|
|
36
|
+
const authQueryParams = authManager.getAuthQueryParams(tool.authConfig);
|
|
37
|
+
for (const [key, value] of Object.entries(authQueryParams)) {
|
|
38
|
+
urlObj.searchParams.set(key, value);
|
|
39
|
+
}
|
|
35
40
|
}
|
|
36
41
|
url = urlObj.toString();
|
|
37
42
|
// Build request options
|
|
38
43
|
const headers = {
|
|
39
|
-
...authManager.getAuthHeaders(),
|
|
44
|
+
...(toolRequiresAuth ? authManager.getAuthHeaders(tool.authConfig) : {}),
|
|
40
45
|
};
|
|
41
46
|
const fetchOptions = {
|
|
42
47
|
method: tool.httpMethod,
|
|
@@ -62,7 +67,29 @@ function createExecutor(tool, baseUrl, authManager) {
|
|
|
62
67
|
responseData = await response.text();
|
|
63
68
|
}
|
|
64
69
|
if (!response.ok) {
|
|
65
|
-
|
|
70
|
+
// Build detailed error message
|
|
71
|
+
const errorDetails = [
|
|
72
|
+
`HTTP ${response.status} ${response.statusText}`,
|
|
73
|
+
`URL: ${tool.httpMethod} ${url}`,
|
|
74
|
+
`Response: ${typeof responseData === 'string' ? responseData : JSON.stringify(responseData, null, 2)}`
|
|
75
|
+
];
|
|
76
|
+
// Add helpful context for common errors
|
|
77
|
+
if (response.status === 401) {
|
|
78
|
+
errorDetails.push('Authentication failed. Check your API key/token.');
|
|
79
|
+
}
|
|
80
|
+
else if (response.status === 403) {
|
|
81
|
+
errorDetails.push('Access forbidden. Verify your permissions.');
|
|
82
|
+
}
|
|
83
|
+
else if (response.status === 404) {
|
|
84
|
+
errorDetails.push('Resource not found.');
|
|
85
|
+
}
|
|
86
|
+
else if (response.status === 429) {
|
|
87
|
+
errorDetails.push('Rate limit exceeded. Try again later.');
|
|
88
|
+
}
|
|
89
|
+
else if (response.status >= 500) {
|
|
90
|
+
errorDetails.push('Server error. The API service may be experiencing issues.');
|
|
91
|
+
}
|
|
92
|
+
throw new Error(errorDetails.join('\n'));
|
|
66
93
|
}
|
|
67
94
|
return responseData;
|
|
68
95
|
}
|
|
@@ -95,39 +122,55 @@ function buildUrl(baseUrl, path, params) {
|
|
|
95
122
|
return `${baseUrl}${finalPath}`;
|
|
96
123
|
}
|
|
97
124
|
/**
|
|
98
|
-
* Separate parameters into path, query, and body params
|
|
125
|
+
* Separate parameters into path, query, and body params using metadata
|
|
99
126
|
*/
|
|
100
|
-
function separateParams(params,
|
|
127
|
+
function separateParams(params, metadata) {
|
|
101
128
|
const pathParams = {};
|
|
102
129
|
const queryParams = {};
|
|
103
130
|
const bodyParams = {};
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
147
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
}
|
|
122
163
|
}
|
|
123
164
|
}
|
|
124
|
-
// For simplicity, if there are non-path primitive params,
|
|
125
|
-
// we need to determine if they're query or body based on HTTP method
|
|
126
|
-
// Since we don't have that info here, we'll treat all non-path primitives
|
|
127
|
-
// as potential query params for GET/DELETE, and body params for POST/PUT/PATCH
|
|
128
|
-
// This is handled by the executor which knows the method
|
|
129
165
|
return { pathParams, queryParams, bodyParams };
|
|
130
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
|
+
}
|
|
131
174
|
/**
|
|
132
175
|
* Check if value is a primitive type
|
|
133
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;
|
|
@@ -7,8 +15,10 @@ export interface Tool {
|
|
|
7
15
|
execute: (params: unknown) => Promise<unknown>;
|
|
8
16
|
httpMethod: HttpMethod;
|
|
9
17
|
path: string;
|
|
18
|
+
authConfig?: AuthConfig;
|
|
19
|
+
parameterMetadata?: ParameterMetadata;
|
|
10
20
|
}
|
|
11
|
-
export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'none';
|
|
21
|
+
export type AuthType = 'oauth2' | 'apiKey' | 'bearer' | 'basic' | 'none';
|
|
12
22
|
export interface AuthConfig {
|
|
13
23
|
type: AuthType;
|
|
14
24
|
authorizationUrl?: string;
|