@sync.so/mcp-server 0.1.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/auth/api-key.d.ts +6 -0
  4. package/dist/auth/api-key.d.ts.map +1 -0
  5. package/dist/auth/api-key.js +6 -0
  6. package/dist/auth/device-auth.d.ts +6 -0
  7. package/dist/auth/device-auth.d.ts.map +1 -0
  8. package/dist/auth/device-auth.js +44 -0
  9. package/dist/auth/index.d.ts +6 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +3 -0
  12. package/dist/auth/token-store.d.ts +4 -0
  13. package/dist/auth/token-store.d.ts.map +1 -0
  14. package/dist/auth/token-store.js +34 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +71 -0
  18. package/dist/config.d.ts +9 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +13 -0
  21. package/dist/http-client.d.ts +11 -0
  22. package/dist/http-client.d.ts.map +1 -0
  23. package/dist/http-client.js +43 -0
  24. package/dist/index.d.ts +12 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +6 -0
  27. package/dist/openapi/fetcher.d.ts +4 -0
  28. package/dist/openapi/fetcher.d.ts.map +1 -0
  29. package/dist/openapi/fetcher.js +13 -0
  30. package/dist/openapi/index.d.ts +4 -0
  31. package/dist/openapi/index.d.ts.map +1 -0
  32. package/dist/openapi/index.js +2 -0
  33. package/dist/openapi/parser.d.ts +5 -0
  34. package/dist/openapi/parser.d.ts.map +1 -0
  35. package/dist/openapi/parser.js +101 -0
  36. package/dist/openapi/parser.test.d.ts +2 -0
  37. package/dist/openapi/parser.test.d.ts.map +1 -0
  38. package/dist/openapi/parser.test.js +188 -0
  39. package/dist/openapi/types.d.ts +55 -0
  40. package/dist/openapi/types.d.ts.map +1 -0
  41. package/dist/openapi/types.js +1 -0
  42. package/dist/server.d.ts +4 -0
  43. package/dist/server.d.ts.map +1 -0
  44. package/dist/server.js +65 -0
  45. package/dist/tools/generator.d.ts +12 -0
  46. package/dist/tools/generator.d.ts.map +1 -0
  47. package/dist/tools/generator.js +154 -0
  48. package/dist/tools/generator.test.d.ts +2 -0
  49. package/dist/tools/generator.test.d.ts.map +1 -0
  50. package/dist/tools/generator.test.js +178 -0
  51. package/dist/tools/index.d.ts +6 -0
  52. package/dist/tools/index.d.ts.map +1 -0
  53. package/dist/tools/index.js +3 -0
  54. package/dist/tools/overrides.d.ts +6 -0
  55. package/dist/tools/overrides.d.ts.map +1 -0
  56. package/dist/tools/overrides.js +29 -0
  57. package/dist/tools/registry.d.ts +8 -0
  58. package/dist/tools/registry.d.ts.map +1 -0
  59. package/dist/tools/registry.js +18 -0
  60. package/package.json +54 -0
@@ -0,0 +1,101 @@
1
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
2
+ export function parseSpec(spec) {
3
+ const operations = [];
4
+ for (const [path, pathItem] of Object.entries(spec.paths)) {
5
+ for (const method of HTTP_METHODS) {
6
+ const operation = pathItem[method];
7
+ if (!operation)
8
+ continue;
9
+ const tags = operation.tags ?? [];
10
+ if (tags.includes('internal'))
11
+ continue;
12
+ const operationId = operation.operationId;
13
+ if (!operationId)
14
+ continue;
15
+ const parameters = parseParameters(operation.parameters ?? [], spec);
16
+ const requestBody = parseRequestBody(operation.requestBody, spec);
17
+ const isMultipart = requestBody?.contentType === 'multipart/form-data';
18
+ operations.push({
19
+ operationId,
20
+ method,
21
+ path,
22
+ summary: operation.summary ?? operationId,
23
+ description: operation.description,
24
+ tags,
25
+ parameters,
26
+ requestBody,
27
+ isMultipart,
28
+ });
29
+ }
30
+ }
31
+ return operations;
32
+ }
33
+ function parseParameters(params, spec) {
34
+ return params.map((param) => {
35
+ const resolved = param.$ref ? resolveRef(spec, param.$ref) : param;
36
+ return {
37
+ name: resolved.name,
38
+ in: resolved.in,
39
+ required: resolved.required ?? false,
40
+ schema: resolveSchemaRefs(resolved.schema ?? { type: 'string' }, spec),
41
+ description: resolved.description,
42
+ };
43
+ });
44
+ }
45
+ function parseRequestBody(requestBody, spec) {
46
+ if (!requestBody?.content)
47
+ return undefined;
48
+ const contentType = 'application/json' in requestBody.content
49
+ ? 'application/json'
50
+ : 'multipart/form-data' in requestBody.content
51
+ ? 'multipart/form-data'
52
+ : Object.keys(requestBody.content)[0];
53
+ if (!contentType)
54
+ return undefined;
55
+ const mediaType = requestBody.content[contentType];
56
+ if (!mediaType?.schema)
57
+ return undefined;
58
+ return {
59
+ schema: resolveSchemaRefs(mediaType.schema, spec),
60
+ contentType,
61
+ };
62
+ }
63
+ export function resolveRef(spec, ref) {
64
+ const parts = ref.replace('#/', '').split('/');
65
+ let current = spec;
66
+ for (const part of parts) {
67
+ if (current == null || typeof current !== 'object') {
68
+ throw new Error(`Cannot resolve $ref: ${ref}`);
69
+ }
70
+ current = current[part];
71
+ }
72
+ return current;
73
+ }
74
+ export function resolveSchemaRefs(schema, spec) {
75
+ if (schema.$ref) {
76
+ const resolved = resolveRef(spec, schema.$ref);
77
+ return resolveSchemaRefs(resolved, spec);
78
+ }
79
+ const result = {};
80
+ for (const [key, value] of Object.entries(schema)) {
81
+ if (key === 'properties' && typeof value === 'object' && value !== null) {
82
+ const props = {};
83
+ for (const [propName, propSchema] of Object.entries(value)) {
84
+ props[propName] = resolveSchemaRefs(propSchema, spec);
85
+ }
86
+ result[key] = props;
87
+ }
88
+ else if ((key === 'items' || key === 'additionalProperties') &&
89
+ typeof value === 'object' &&
90
+ value !== null) {
91
+ result[key] = resolveSchemaRefs(value, spec);
92
+ }
93
+ else if (key === 'allOf' || key === 'oneOf' || key === 'anyOf') {
94
+ result[key] = value.map((s) => resolveSchemaRefs(s, spec));
95
+ }
96
+ else {
97
+ result[key] = value;
98
+ }
99
+ }
100
+ return result;
101
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.test.d.ts","sourceRoot":"","sources":["../../src/openapi/parser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseSpec, resolveRef, resolveSchemaRefs } from './parser.js';
3
+ const MINIMAL_SPEC = {
4
+ openapi: '3.1.0',
5
+ paths: {
6
+ '/v2/generate': {
7
+ post: {
8
+ operationId: 'GenerateController_createGeneration',
9
+ summary: 'Create a generation',
10
+ tags: ['Generate'],
11
+ parameters: [],
12
+ requestBody: {
13
+ content: {
14
+ 'application/json': {
15
+ schema: {
16
+ type: 'object',
17
+ properties: {
18
+ model: { type: 'string' },
19
+ input: { type: 'array', items: { type: 'object' } },
20
+ },
21
+ required: ['model', 'input'],
22
+ },
23
+ },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ '/v2/generate/{id}': {
29
+ get: {
30
+ operationId: 'GenerateController_getGeneration',
31
+ summary: 'Get a generation by ID',
32
+ tags: ['Generate'],
33
+ parameters: [
34
+ {
35
+ name: 'id',
36
+ in: 'path',
37
+ required: true,
38
+ schema: { type: 'string' },
39
+ },
40
+ ],
41
+ },
42
+ },
43
+ '/v2/internal/something': {
44
+ get: {
45
+ operationId: 'InternalController_doSomething',
46
+ summary: 'Internal endpoint',
47
+ tags: ['internal'],
48
+ parameters: [],
49
+ },
50
+ },
51
+ },
52
+ };
53
+ describe('parseSpec', () => {
54
+ it('parses operations from OpenAPI spec', () => {
55
+ const operations = parseSpec(MINIMAL_SPEC);
56
+ expect(operations).toHaveLength(2);
57
+ });
58
+ it('extracts operation details correctly', () => {
59
+ const operations = parseSpec(MINIMAL_SPEC);
60
+ const createOp = operations.find((op) => op.operationId === 'GenerateController_createGeneration');
61
+ expect(createOp).toBeDefined();
62
+ expect(createOp?.method).toBe('post');
63
+ expect(createOp?.path).toBe('/v2/generate');
64
+ expect(createOp?.summary).toBe('Create a generation');
65
+ expect(createOp?.requestBody).toBeDefined();
66
+ });
67
+ it('parses path parameters', () => {
68
+ const operations = parseSpec(MINIMAL_SPEC);
69
+ const getOp = operations.find((op) => op.operationId === 'GenerateController_getGeneration');
70
+ expect(getOp).toBeDefined();
71
+ expect(getOp?.parameters).toHaveLength(1);
72
+ expect(getOp?.parameters[0]?.name).toBe('id');
73
+ expect(getOp?.parameters[0]?.in).toBe('path');
74
+ expect(getOp?.parameters[0]?.required).toBe(true);
75
+ });
76
+ it('filters out internal-tagged endpoints', () => {
77
+ const operations = parseSpec(MINIMAL_SPEC);
78
+ const internalOp = operations.find((op) => op.operationId === 'InternalController_doSomething');
79
+ expect(internalOp).toBeUndefined();
80
+ });
81
+ it('skips operations without operationId', () => {
82
+ const spec = {
83
+ openapi: '3.1.0',
84
+ paths: {
85
+ '/v2/test': {
86
+ get: {
87
+ summary: 'No operation ID',
88
+ tags: ['Test'],
89
+ },
90
+ },
91
+ },
92
+ };
93
+ const operations = parseSpec(spec);
94
+ expect(operations).toHaveLength(0);
95
+ });
96
+ it('detects multipart endpoints', () => {
97
+ const spec = {
98
+ openapi: '3.1.0',
99
+ paths: {
100
+ '/v2/upload': {
101
+ post: {
102
+ operationId: 'UploadController_upload',
103
+ summary: 'Upload a file',
104
+ tags: ['Upload'],
105
+ parameters: [],
106
+ requestBody: {
107
+ content: {
108
+ 'multipart/form-data': {
109
+ schema: { type: 'object' },
110
+ },
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ };
117
+ const operations = parseSpec(spec);
118
+ expect(operations[0]?.isMultipart).toBe(true);
119
+ });
120
+ });
121
+ describe('resolveRef', () => {
122
+ it('resolves $ref to component schemas', () => {
123
+ const spec = {
124
+ openapi: '3.1.0',
125
+ paths: {},
126
+ components: {
127
+ schemas: {
128
+ MyModel: { type: 'object', properties: { name: { type: 'string' } } },
129
+ },
130
+ },
131
+ };
132
+ const resolved = resolveRef(spec, '#/components/schemas/MyModel');
133
+ expect(resolved).toEqual({
134
+ type: 'object',
135
+ properties: { name: { type: 'string' } },
136
+ });
137
+ });
138
+ });
139
+ describe('resolveSchemaRefs', () => {
140
+ const spec = {
141
+ openapi: '3.1.0',
142
+ paths: {},
143
+ components: {
144
+ schemas: {
145
+ Status: { type: 'string', enum: ['pending', 'completed'] },
146
+ },
147
+ },
148
+ };
149
+ it('resolves $ref in schema', () => {
150
+ const schema = { $ref: '#/components/schemas/Status' };
151
+ const resolved = resolveSchemaRefs(schema, spec);
152
+ expect(resolved).toEqual({
153
+ type: 'string',
154
+ enum: ['pending', 'completed'],
155
+ });
156
+ });
157
+ it('passes through schemas without $ref', () => {
158
+ const schema = { type: 'string' };
159
+ const resolved = resolveSchemaRefs(schema, spec);
160
+ expect(resolved).toEqual({ type: 'string' });
161
+ });
162
+ it('resolves nested $ref in properties', () => {
163
+ const schema = {
164
+ type: 'object',
165
+ properties: {
166
+ status: { $ref: '#/components/schemas/Status' },
167
+ },
168
+ };
169
+ const resolved = resolveSchemaRefs(schema, spec);
170
+ expect(resolved).toEqual({
171
+ type: 'object',
172
+ properties: {
173
+ status: { type: 'string', enum: ['pending', 'completed'] },
174
+ },
175
+ });
176
+ });
177
+ it('resolves $ref in additionalProperties', () => {
178
+ const schema = {
179
+ type: 'object',
180
+ additionalProperties: { $ref: '#/components/schemas/Status' },
181
+ };
182
+ const resolved = resolveSchemaRefs(schema, spec);
183
+ expect(resolved).toEqual({
184
+ type: 'object',
185
+ additionalProperties: { type: 'string', enum: ['pending', 'completed'] },
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,55 @@
1
+ export type ParsedParameter = {
2
+ name: string;
3
+ in: 'path' | 'query' | 'header';
4
+ required: boolean;
5
+ schema: JsonSchema;
6
+ description?: string;
7
+ };
8
+ export type ParsedOperation = {
9
+ operationId: string;
10
+ method: 'get' | 'post' | 'put' | 'patch' | 'delete';
11
+ path: string;
12
+ summary: string;
13
+ description?: string;
14
+ tags: string[];
15
+ parameters: ParsedParameter[];
16
+ requestBody?: {
17
+ schema: JsonSchema;
18
+ contentType: string;
19
+ };
20
+ isMultipart: boolean;
21
+ };
22
+ export type JsonSchema = Record<string, unknown>;
23
+ export type OpenApiSpec = {
24
+ openapi: string;
25
+ paths: Record<string, Record<string, OpenApiOperation>>;
26
+ components?: {
27
+ schemas?: Record<string, JsonSchema>;
28
+ [key: string]: unknown;
29
+ };
30
+ [key: string]: unknown;
31
+ };
32
+ type OpenApiOperation = {
33
+ operationId?: string;
34
+ summary?: string;
35
+ description?: string;
36
+ tags?: string[];
37
+ parameters?: OpenApiParameter[];
38
+ requestBody?: {
39
+ content: Record<string, {
40
+ schema: JsonSchema;
41
+ }>;
42
+ required?: boolean;
43
+ };
44
+ [key: string]: unknown;
45
+ };
46
+ type OpenApiParameter = {
47
+ name: string;
48
+ in: 'path' | 'query' | 'header';
49
+ required?: boolean;
50
+ schema: JsonSchema;
51
+ description?: string;
52
+ $ref?: string;
53
+ };
54
+ export {};
55
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/openapi/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACxD,UAAU,CAAC,EAAE;QACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,MAAM,EAAE,UAAU,CAAA;SAAE,CAAC,CAAC;QAChD,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { SyncMcpConfig } from './config.js';
3
+ export declare function createSyncMcpServer(config: SyncMcpConfig): Promise<McpServer>;
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,CA6CnF"}
package/dist/server.js ADDED
@@ -0,0 +1,65 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { createApiKeyAuth } from './auth/api-key.js';
3
+ import { performDeviceAuth } from './auth/device-auth.js';
4
+ import { loadToken } from './auth/token-store.js';
5
+ import { createHttpClient } from './http-client.js';
6
+ import { fetchSpec } from './openapi/fetcher.js';
7
+ import { parseSpec } from './openapi/parser.js';
8
+ import { generateTools } from './tools/generator.js';
9
+ export async function createSyncMcpServer(config) {
10
+ const log = (message) => {
11
+ process.stderr.write(message);
12
+ };
13
+ const authHeaders = await resolveAuth(config, log);
14
+ const httpClient = createHttpClient(config.baseUrl, authHeaders);
15
+ log(`Fetching OpenAPI spec from ${config.baseUrl}...\n`);
16
+ const spec = await fetchSpec(config.baseUrl);
17
+ const operations = parseSpec(spec);
18
+ log(`Discovered ${operations.length} API operations\n`);
19
+ const tools = generateTools(operations, httpClient);
20
+ const server = new McpServer({
21
+ name: 'sync',
22
+ version: '0.1.0',
23
+ });
24
+ for (const tool of tools) {
25
+ server.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
26
+ try {
27
+ const result = await tool.handler((args ?? {}));
28
+ return {
29
+ content: [
30
+ {
31
+ type: 'text',
32
+ text: JSON.stringify(result, null, 2),
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return {
40
+ content: [{ type: 'text', text: `Error: ${message}` }],
41
+ isError: true,
42
+ };
43
+ }
44
+ });
45
+ }
46
+ log(`Registered ${tools.length} MCP tools\n`);
47
+ return server;
48
+ }
49
+ async function resolveAuth(config, log) {
50
+ if (config.apiKey) {
51
+ log('Using API key authentication\n');
52
+ return createApiKeyAuth(config.apiKey).headers;
53
+ }
54
+ const cachedToken = await loadToken();
55
+ if (cachedToken) {
56
+ log('Using cached device auth token\n');
57
+ return {
58
+ Authorization: `Bearer ${cachedToken}`,
59
+ 'x-sync-source': 'mcp',
60
+ };
61
+ }
62
+ log('No API key or cached token found. Starting device auth...\n');
63
+ const auth = await performDeviceAuth(config.baseUrl, log);
64
+ return auth.headers;
65
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ import type { HttpClient } from '../http-client.js';
3
+ import type { ParsedOperation } from '../openapi/types.js';
4
+ export type McpToolDefinition = {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: Record<string, z.ZodType>;
8
+ handler: (args: Record<string, unknown>) => Promise<unknown>;
9
+ };
10
+ export declare function generateTools(operations: ParsedOperation[], httpClient: HttpClient): McpToolDefinition[];
11
+ export declare function operationIdToToolName(operationId: string): string;
12
+ //# sourceMappingURL=generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/tools/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGvE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D,CAAC;AAEF,wBAAgB,aAAa,CAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,UAAU,EAAE,UAAU,GACrB,iBAAiB,EAAE,CAErB;AAuBD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQjE"}
@@ -0,0 +1,154 @@
1
+ import { z } from 'zod';
2
+ import { getOverride } from './overrides.js';
3
+ export function generateTools(operations, httpClient) {
4
+ return operations.map((op) => generateTool(op, httpClient));
5
+ }
6
+ function generateTool(operation, httpClient) {
7
+ const name = operationIdToToolName(operation.operationId);
8
+ const override = getOverride(name);
9
+ const description = override?.description ?? truncate(operation.summary, 200);
10
+ const inputSchema = buildInputSchema(operation);
11
+ return {
12
+ name,
13
+ description,
14
+ inputSchema,
15
+ handler: async (args) => {
16
+ const path = buildPath(operation.path, args);
17
+ const query = buildQuery(operation.parameters, args);
18
+ const body = buildBody(operation, args);
19
+ return httpClient.request(operation.method, path, { query, body });
20
+ },
21
+ };
22
+ }
23
+ export function operationIdToToolName(operationId) {
24
+ // "GenerateController_createGeneration" → "generate_create-generation"
25
+ const parts = operationId.split('_');
26
+ if (parts.length < 2)
27
+ return toKebab(operationId);
28
+ const controller = toKebab(parts[0].replace(/Controller$/, ''));
29
+ const method = toKebab(parts.slice(1).join('_'));
30
+ return `${controller}_${method}`;
31
+ }
32
+ function toKebab(str) {
33
+ return str
34
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
35
+ .replace(/_/g, '-')
36
+ .toLowerCase();
37
+ }
38
+ function truncate(str, maxLen) {
39
+ if (str.length <= maxLen)
40
+ return str;
41
+ return `${str.slice(0, maxLen - 3)}...`;
42
+ }
43
+ function buildInputSchema(operation) {
44
+ const shape = {};
45
+ for (const param of operation.parameters) {
46
+ if (param.in === 'header')
47
+ continue;
48
+ const zodType = jsonSchemaToZod(param.schema, param.description);
49
+ shape[param.name] = param.required ? zodType : zodType.optional();
50
+ }
51
+ if (operation.requestBody?.schema) {
52
+ const bodySchema = operation.requestBody.schema;
53
+ const flatSchemas = flattenComposedSchema(bodySchema);
54
+ for (const sub of flatSchemas) {
55
+ if (!sub.properties)
56
+ continue;
57
+ const properties = sub.properties;
58
+ const required = sub.required ?? [];
59
+ for (const [propName, propSchema] of Object.entries(properties)) {
60
+ const zodType = jsonSchemaToZod(propSchema, propSchema.description);
61
+ shape[propName] = required.includes(propName) ? zodType : zodType.optional();
62
+ }
63
+ }
64
+ }
65
+ return shape;
66
+ }
67
+ function flattenComposedSchema(schema) {
68
+ const results = [];
69
+ if (schema.properties)
70
+ results.push(schema);
71
+ for (const key of ['allOf', 'oneOf', 'anyOf']) {
72
+ const composed = schema[key];
73
+ if (composed)
74
+ results.push(...composed.flatMap(flattenComposedSchema));
75
+ }
76
+ return results.length > 0 ? results : [schema];
77
+ }
78
+ function jsonSchemaToZod(schema, description) {
79
+ let zodType;
80
+ const type = schema.type;
81
+ const enumValues = schema.enum;
82
+ if (enumValues) {
83
+ zodType = z.enum(enumValues);
84
+ }
85
+ else if (type === 'string') {
86
+ zodType = z.string();
87
+ }
88
+ else if (type === 'number' || type === 'integer') {
89
+ zodType = z.number();
90
+ }
91
+ else if (type === 'boolean') {
92
+ zodType = z.boolean();
93
+ }
94
+ else if (type === 'array') {
95
+ const items = schema.items;
96
+ zodType = z.array(items ? jsonSchemaToZod(items) : z.unknown());
97
+ }
98
+ else if (type === 'object') {
99
+ const properties = schema.properties;
100
+ if (properties) {
101
+ const required = schema.required ?? [];
102
+ const shape = {};
103
+ for (const [propName, propSchema] of Object.entries(properties)) {
104
+ const innerType = jsonSchemaToZod(propSchema);
105
+ shape[propName] = required.includes(propName) ? innerType : innerType.optional();
106
+ }
107
+ zodType = z.object(shape);
108
+ }
109
+ else {
110
+ zodType = z.record(z.string(), z.unknown());
111
+ }
112
+ }
113
+ else {
114
+ zodType = z.unknown();
115
+ }
116
+ if (description) {
117
+ zodType = zodType.describe(description);
118
+ }
119
+ return zodType;
120
+ }
121
+ function buildPath(pathTemplate, args) {
122
+ return pathTemplate.replace(/\{(\w+)\}/g, (_match, paramName) => {
123
+ const value = args[paramName];
124
+ if (value === undefined) {
125
+ throw new Error(`Missing required path parameter: ${paramName}`);
126
+ }
127
+ return encodeURIComponent(String(value));
128
+ });
129
+ }
130
+ function buildQuery(parameters, args) {
131
+ const queryParams = parameters.filter((p) => p.in === 'query');
132
+ if (queryParams.length === 0)
133
+ return undefined;
134
+ const query = {};
135
+ for (const param of queryParams) {
136
+ const value = args[param.name];
137
+ if (value !== undefined && value !== null) {
138
+ query[param.name] = String(value);
139
+ }
140
+ }
141
+ return Object.keys(query).length > 0 ? query : undefined;
142
+ }
143
+ function buildBody(operation, args) {
144
+ if (!operation.requestBody)
145
+ return undefined;
146
+ const pathAndQueryNames = new Set(operation.parameters.filter((p) => p.in !== 'header').map((p) => p.name));
147
+ const body = {};
148
+ for (const [key, value] of Object.entries(args)) {
149
+ if (!pathAndQueryNames.has(key) && value !== undefined) {
150
+ body[key] = value;
151
+ }
152
+ }
153
+ return Object.keys(body).length > 0 ? body : undefined;
154
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.test.d.ts","sourceRoot":"","sources":["../../src/tools/generator.test.ts"],"names":[],"mappings":""}