@notionhq/notion-mcp-server 1.0.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.
@@ -0,0 +1,40 @@
1
+ import { OpenAPIV3 } from 'openapi-types'
2
+
3
+ /**
4
+ * Identifies file upload parameters in an OpenAPI operation
5
+ * @param operation The OpenAPI operation object to check
6
+ * @returns Array of parameter names that are file uploads
7
+ */
8
+ export function isFileUploadParameter(operation: OpenAPIV3.OperationObject): string[] {
9
+ const fileParams: string[] = []
10
+
11
+ if (!operation.requestBody) return fileParams
12
+
13
+ const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject
14
+ const content = requestBody.content || {}
15
+
16
+ // Check multipart/form-data content type for file uploads
17
+ const multipartContent = content['multipart/form-data']
18
+ if (!multipartContent?.schema) return fileParams
19
+
20
+ const schema = multipartContent.schema as OpenAPIV3.SchemaObject
21
+ if (schema.type !== 'object' || !schema.properties) return fileParams
22
+
23
+ // Look for properties with type: string, format: binary which indicates file uploads
24
+ Object.entries(schema.properties).forEach(([propName, prop]) => {
25
+ const schemaProp = prop as OpenAPIV3.SchemaObject
26
+ if (schemaProp.type === 'string' && schemaProp.format === 'binary') {
27
+ fileParams.push(propName)
28
+ }
29
+
30
+ // Check for array of files
31
+ if (schemaProp.type === 'array' && schemaProp.items) {
32
+ const itemSchema = schemaProp.items as OpenAPIV3.SchemaObject
33
+ if (itemSchema.type === 'string' && itemSchema.format === 'binary') {
34
+ fileParams.push(propName)
35
+ }
36
+ }
37
+ })
38
+
39
+ return fileParams
40
+ }
@@ -0,0 +1,519 @@
1
+ import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2
+ import type { JSONSchema7 as IJsonSchema } from 'json-schema'
3
+ import type { ChatCompletionTool } from 'openai/resources/chat/completions'
4
+ import type { Tool } from '@anthropic-ai/sdk/resources/messages/messages'
5
+
6
+ type NewToolMethod = {
7
+ name: string
8
+ description: string
9
+ inputSchema: IJsonSchema & { type: 'object' }
10
+ returnSchema?: IJsonSchema
11
+ }
12
+
13
+ type FunctionParameters = {
14
+ type: 'object'
15
+ properties?: Record<string, unknown>
16
+ required?: string[]
17
+ [key: string]: unknown
18
+ }
19
+
20
+ export class OpenAPIToMCPConverter {
21
+ private schemaCache: Record<string, IJsonSchema> = {}
22
+ private nameCounter: number = 0
23
+
24
+ constructor(private openApiSpec: OpenAPIV3.Document | OpenAPIV3_1.Document) {}
25
+
26
+ /**
27
+ * Resolve a $ref reference to its schema in the openApiSpec.
28
+ * Returns the raw OpenAPI SchemaObject or null if not found.
29
+ */
30
+ private internalResolveRef(ref: string, resolvedRefs: Set<string>): OpenAPIV3.SchemaObject | null {
31
+ if (!ref.startsWith('#/')) {
32
+ return null
33
+ }
34
+ if (resolvedRefs.has(ref)) {
35
+ return null
36
+ }
37
+
38
+ const parts = ref.replace(/^#\//, '').split('/')
39
+ let current: any = this.openApiSpec
40
+ for (const part of parts) {
41
+ current = current[part]
42
+ if (!current) return null
43
+ }
44
+ resolvedRefs.add(ref)
45
+ return current as OpenAPIV3.SchemaObject
46
+ }
47
+
48
+ /**
49
+ * Convert an OpenAPI schema (or reference) into a JSON Schema object.
50
+ * Uses caching and handles cycles by returning $ref nodes.
51
+ */
52
+ convertOpenApiSchemaToJsonSchema(
53
+ schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
54
+ resolvedRefs: Set<string>,
55
+ resolveRefs: boolean = false,
56
+ ): IJsonSchema {
57
+ if ('$ref' in schema) {
58
+ const ref = schema.$ref
59
+ if (!resolveRefs) {
60
+ if (ref.startsWith('#/components/schemas/')) {
61
+ return {
62
+ $ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
63
+ ...('description' in schema ? { description: schema.description as string } : {}),
64
+ }
65
+ }
66
+ console.error(`Attempting to resolve ref ${ref} not found in components collection.`)
67
+ // deliberate fall through
68
+ }
69
+ // Create base schema with $ref and description if present
70
+ const refSchema: IJsonSchema = { $ref: ref }
71
+ if ('description' in schema && schema.description) {
72
+ refSchema.description = schema.description as string
73
+ }
74
+
75
+ // If already cached, return immediately with description
76
+ if (this.schemaCache[ref]) {
77
+ return this.schemaCache[ref]
78
+ }
79
+
80
+ const resolved = this.internalResolveRef(ref, resolvedRefs)
81
+ if (!resolved) {
82
+ // TODO: need extensive tests for this and we definitely need to handle the case of self references
83
+ console.error(`Failed to resolve ref ${ref}`)
84
+ return {
85
+ $ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
86
+ description: 'description' in schema ? ((schema.description as string) ?? '') : '',
87
+ }
88
+ } else {
89
+ const converted = this.convertOpenApiSchemaToJsonSchema(resolved, resolvedRefs, resolveRefs)
90
+ this.schemaCache[ref] = converted
91
+
92
+ return converted
93
+ }
94
+ }
95
+
96
+ // Handle inline schema
97
+ const result: IJsonSchema = {}
98
+
99
+ if (schema.type) {
100
+ result.type = schema.type as IJsonSchema['type']
101
+ }
102
+
103
+ // Convert binary format to uri-reference and enhance description
104
+ if (schema.format === 'binary') {
105
+ result.format = 'uri-reference'
106
+ const binaryDesc = 'absolute paths to local files'
107
+ result.description = schema.description ? `${schema.description} (${binaryDesc})` : binaryDesc
108
+ } else {
109
+ if (schema.format) {
110
+ result.format = schema.format
111
+ }
112
+ if (schema.description) {
113
+ result.description = schema.description
114
+ }
115
+ }
116
+
117
+ if (schema.enum) {
118
+ result.enum = schema.enum
119
+ }
120
+
121
+ if (schema.default !== undefined) {
122
+ result.default = schema.default
123
+ }
124
+
125
+ // Handle object properties
126
+ if (schema.type === 'object') {
127
+ result.type = 'object'
128
+ if (schema.properties) {
129
+ result.properties = {}
130
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
131
+ result.properties[name] = this.convertOpenApiSchemaToJsonSchema(propSchema, resolvedRefs, resolveRefs)
132
+ }
133
+ }
134
+ if (schema.required) {
135
+ result.required = schema.required
136
+ }
137
+ if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
138
+ result.additionalProperties = true
139
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
140
+ result.additionalProperties = this.convertOpenApiSchemaToJsonSchema(schema.additionalProperties, resolvedRefs, resolveRefs)
141
+ } else {
142
+ result.additionalProperties = false
143
+ }
144
+ }
145
+
146
+ // Handle arrays - ensure binary format conversion happens for array items too
147
+ if (schema.type === 'array' && schema.items) {
148
+ result.type = 'array'
149
+ result.items = this.convertOpenApiSchemaToJsonSchema(schema.items, resolvedRefs, resolveRefs)
150
+ }
151
+
152
+ // oneOf, anyOf, allOf
153
+ if (schema.oneOf) {
154
+ result.oneOf = schema.oneOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
155
+ }
156
+ if (schema.anyOf) {
157
+ result.anyOf = schema.anyOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
158
+ }
159
+ if (schema.allOf) {
160
+ result.allOf = schema.allOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
161
+ }
162
+
163
+ return result
164
+ }
165
+
166
+ convertToMCPTools(): {
167
+ tools: Record<string, { methods: NewToolMethod[] }>
168
+ openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
169
+ zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }>
170
+ } {
171
+ const apiName = 'API'
172
+
173
+ const openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }> = {}
174
+ const tools: Record<string, { methods: NewToolMethod[] }> = {
175
+ [apiName]: { methods: [] },
176
+ }
177
+ const zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }> = {}
178
+ for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
179
+ if (!pathItem) continue
180
+
181
+ for (const [method, operation] of Object.entries(pathItem)) {
182
+ if (!this.isOperation(method, operation)) continue
183
+
184
+ const mcpMethod = this.convertOperationToMCPMethod(operation, method, path)
185
+ if (mcpMethod) {
186
+ const uniqueName = this.ensureUniqueName(mcpMethod.name)
187
+ mcpMethod.name = uniqueName
188
+ tools[apiName]!.methods.push(mcpMethod)
189
+ openApiLookup[apiName + '-' + uniqueName] = { ...operation, method, path }
190
+ zip[apiName + '-' + uniqueName] = { openApi: { ...operation, method, path }, mcp: mcpMethod }
191
+ }
192
+ }
193
+ }
194
+
195
+ return { tools, openApiLookup, zip }
196
+ }
197
+
198
+ /**
199
+ * Convert the OpenAPI spec to OpenAI's ChatCompletionTool format
200
+ */
201
+ convertToOpenAITools(): ChatCompletionTool[] {
202
+ const tools: ChatCompletionTool[] = []
203
+
204
+ for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
205
+ if (!pathItem) continue
206
+
207
+ for (const [method, operation] of Object.entries(pathItem)) {
208
+ if (!this.isOperation(method, operation)) continue
209
+
210
+ const parameters = this.convertOperationToJsonSchema(operation, method, path)
211
+ const tool: ChatCompletionTool = {
212
+ type: 'function',
213
+ function: {
214
+ name: operation.operationId!,
215
+ description: operation.summary || operation.description || '',
216
+ parameters: parameters as FunctionParameters,
217
+ },
218
+ }
219
+ tools.push(tool)
220
+ }
221
+ }
222
+
223
+ return tools
224
+ }
225
+
226
+ /**
227
+ * Convert the OpenAPI spec to Anthropic's Tool format
228
+ */
229
+ convertToAnthropicTools(): Tool[] {
230
+ const tools: Tool[] = []
231
+
232
+ for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
233
+ if (!pathItem) continue
234
+
235
+ for (const [method, operation] of Object.entries(pathItem)) {
236
+ if (!this.isOperation(method, operation)) continue
237
+
238
+ const parameters = this.convertOperationToJsonSchema(operation, method, path)
239
+ const tool: Tool = {
240
+ name: operation.operationId!,
241
+ description: operation.summary || operation.description || '',
242
+ input_schema: parameters as Tool['input_schema'],
243
+ }
244
+ tools.push(tool)
245
+ }
246
+ }
247
+
248
+ return tools
249
+ }
250
+
251
+ private convertComponentsToJsonSchema(): Record<string, IJsonSchema> {
252
+ const components = this.openApiSpec.components || {}
253
+ const schema: Record<string, IJsonSchema> = {}
254
+ for (const [key, value] of Object.entries(components.schemas || {})) {
255
+ schema[key] = this.convertOpenApiSchemaToJsonSchema(value, new Set())
256
+ }
257
+ return schema
258
+ }
259
+ /**
260
+ * Helper method to convert an operation to a JSON Schema for parameters
261
+ */
262
+ private convertOperationToJsonSchema(
263
+ operation: OpenAPIV3.OperationObject,
264
+ method: string,
265
+ path: string,
266
+ ): IJsonSchema & { type: 'object' } {
267
+ const schema: IJsonSchema & { type: 'object' } = {
268
+ type: 'object',
269
+ properties: {},
270
+ required: [],
271
+ $defs: this.convertComponentsToJsonSchema(),
272
+ }
273
+
274
+ // Handle parameters (path, query, header, cookie)
275
+ if (operation.parameters) {
276
+ for (const param of operation.parameters) {
277
+ const paramObj = this.resolveParameter(param)
278
+ if (paramObj && paramObj.schema) {
279
+ const paramSchema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set())
280
+ // Merge parameter-level description if available
281
+ if (paramObj.description) {
282
+ paramSchema.description = paramObj.description
283
+ }
284
+ schema.properties![paramObj.name] = paramSchema
285
+ if (paramObj.required) {
286
+ schema.required!.push(paramObj.name)
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ // Handle requestBody
293
+ if (operation.requestBody) {
294
+ const bodyObj = this.resolveRequestBody(operation.requestBody)
295
+ if (bodyObj?.content) {
296
+ if (bodyObj.content['application/json']?.schema) {
297
+ const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set())
298
+ if (bodySchema.type === 'object' && bodySchema.properties) {
299
+ for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
300
+ schema.properties![name] = propSchema
301
+ }
302
+ if (bodySchema.required) {
303
+ schema.required!.push(...bodySchema.required)
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ return schema
311
+ }
312
+
313
+ private isOperation(method: string, operation: any): operation is OpenAPIV3.OperationObject {
314
+ return ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase())
315
+ }
316
+
317
+ private isParameterObject(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): param is OpenAPIV3.ParameterObject {
318
+ return !('$ref' in param)
319
+ }
320
+
321
+ private isRequestBodyObject(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): body is OpenAPIV3.RequestBodyObject {
322
+ return !('$ref' in body)
323
+ }
324
+
325
+ private resolveParameter(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ParameterObject | null {
326
+ if (this.isParameterObject(param)) {
327
+ return param
328
+ } else {
329
+ const resolved = this.internalResolveRef(param.$ref, new Set())
330
+ if (resolved && (resolved as OpenAPIV3.ParameterObject).name) {
331
+ return resolved as OpenAPIV3.ParameterObject
332
+ }
333
+ }
334
+ return null
335
+ }
336
+
337
+ private resolveRequestBody(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): OpenAPIV3.RequestBodyObject | null {
338
+ if (this.isRequestBodyObject(body)) {
339
+ return body
340
+ } else {
341
+ const resolved = this.internalResolveRef(body.$ref, new Set())
342
+ if (resolved) {
343
+ return resolved as OpenAPIV3.RequestBodyObject
344
+ }
345
+ }
346
+ return null
347
+ }
348
+
349
+ private resolveResponse(response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ResponseObject | null {
350
+ if ('$ref' in response) {
351
+ const resolved = this.internalResolveRef(response.$ref, new Set())
352
+ if (resolved) {
353
+ return resolved as OpenAPIV3.ResponseObject
354
+ } else {
355
+ return null
356
+ }
357
+ }
358
+ return response
359
+ }
360
+
361
+ private convertOperationToMCPMethod(operation: OpenAPIV3.OperationObject, method: string, path: string): NewToolMethod | null {
362
+ if (!operation.operationId) {
363
+ console.warn(`Operation without operationId at ${method} ${path}`)
364
+ return null
365
+ }
366
+
367
+ const methodName = operation.operationId
368
+
369
+ const inputSchema: IJsonSchema & { type: 'object' } = {
370
+ $defs: this.convertComponentsToJsonSchema(),
371
+ type: 'object',
372
+ properties: {},
373
+ required: [],
374
+ }
375
+
376
+ // Handle parameters (path, query, header, cookie)
377
+ if (operation.parameters) {
378
+ for (const param of operation.parameters) {
379
+ const paramObj = this.resolveParameter(param)
380
+ if (paramObj && paramObj.schema) {
381
+ const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false)
382
+ // Merge parameter-level description if available
383
+ if (paramObj.description) {
384
+ schema.description = paramObj.description
385
+ }
386
+ inputSchema.properties![paramObj.name] = schema
387
+ if (paramObj.required) {
388
+ inputSchema.required!.push(paramObj.name)
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ // Handle requestBody
395
+ if (operation.requestBody) {
396
+ const bodyObj = this.resolveRequestBody(operation.requestBody)
397
+ if (bodyObj?.content) {
398
+ // Handle multipart/form-data for file uploads
399
+ // We convert the multipart/form-data schema to a JSON schema and we require
400
+ // that the user passes in a string for each file that points to the local file
401
+ if (bodyObj.content['multipart/form-data']?.schema) {
402
+ const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['multipart/form-data'].schema, new Set(), false)
403
+ if (formSchema.type === 'object' && formSchema.properties) {
404
+ for (const [name, propSchema] of Object.entries(formSchema.properties)) {
405
+ inputSchema.properties![name] = propSchema
406
+ }
407
+ if (formSchema.required) {
408
+ inputSchema.required!.push(...formSchema.required!)
409
+ }
410
+ }
411
+ }
412
+ // Handle application/json
413
+ else if (bodyObj.content['application/json']?.schema) {
414
+ const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set(), false)
415
+ // Merge body schema into the inputSchema's properties
416
+ if (bodySchema.type === 'object' && bodySchema.properties) {
417
+ for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
418
+ inputSchema.properties![name] = propSchema
419
+ }
420
+ if (bodySchema.required) {
421
+ inputSchema.required!.push(...bodySchema.required!)
422
+ }
423
+ } else {
424
+ // If the request body is not an object, just put it under "body"
425
+ inputSchema.properties!['body'] = bodySchema
426
+ inputSchema.required!.push('body')
427
+ }
428
+ }
429
+ }
430
+ }
431
+
432
+ // Build description including error responses
433
+ let description = operation.summary || operation.description || ''
434
+ if (operation.responses) {
435
+ const errorResponses = Object.entries(operation.responses)
436
+ .filter(([code]) => code.startsWith('4') || code.startsWith('5'))
437
+ .map(([code, response]) => {
438
+ const responseObj = this.resolveResponse(response)
439
+ let errorDesc = responseObj?.description || ''
440
+ return `${code}: ${errorDesc}`
441
+ })
442
+
443
+ if (errorResponses.length > 0) {
444
+ description += '\nError Responses:\n' + errorResponses.join('\n')
445
+ }
446
+ }
447
+
448
+ // Extract return type (response schema)
449
+ const returnSchema = this.extractResponseType(operation.responses)
450
+
451
+ // Generate Zod schema from input schema
452
+ try {
453
+ // const zodSchemaStr = jsonSchemaToZod(inputSchema, { module: "cjs" })
454
+ // console.log(zodSchemaStr)
455
+ // // Execute the function with the zod instance
456
+ // const zodSchema = eval(zodSchemaStr) as z.ZodType
457
+
458
+ return {
459
+ name: methodName,
460
+ description,
461
+ inputSchema,
462
+ ...(returnSchema ? { returnSchema } : {}),
463
+ }
464
+ } catch (error) {
465
+ console.warn(`Failed to generate Zod schema for ${methodName}:`, error)
466
+ // Fallback to a basic object schema
467
+ return {
468
+ name: methodName,
469
+ description,
470
+ inputSchema,
471
+ ...(returnSchema ? { returnSchema } : {}),
472
+ }
473
+ }
474
+ }
475
+
476
+ private extractResponseType(responses: OpenAPIV3.ResponsesObject | undefined): IJsonSchema | null {
477
+ // Look for a success response
478
+ const successResponse = responses?.['200'] || responses?.['201'] || responses?.['202'] || responses?.['204']
479
+ if (!successResponse) return null
480
+
481
+ const responseObj = this.resolveResponse(successResponse)
482
+ if (!responseObj || !responseObj.content) return null
483
+
484
+ if (responseObj.content['application/json']?.schema) {
485
+ const returnSchema = this.convertOpenApiSchemaToJsonSchema(responseObj.content['application/json'].schema, new Set(), false)
486
+ returnSchema['$defs'] = this.convertComponentsToJsonSchema()
487
+
488
+ // Preserve the response description if available and not already set
489
+ if (responseObj.description && !returnSchema.description) {
490
+ returnSchema.description = responseObj.description
491
+ }
492
+
493
+ return returnSchema
494
+ }
495
+
496
+ // If no JSON response, fallback to a generic string or known formats
497
+ if (responseObj.content['image/png'] || responseObj.content['image/jpeg']) {
498
+ return { type: 'string', format: 'binary', description: responseObj.description || '' }
499
+ }
500
+
501
+ // Fallback
502
+ return { type: 'string', description: responseObj.description || '' }
503
+ }
504
+
505
+ private ensureUniqueName(name: string): string {
506
+ if (name.length <= 64) {
507
+ return name
508
+ }
509
+
510
+ const truncatedName = name.slice(0, 64 - 5) // Reserve space for suffix
511
+ const uniqueSuffix = this.generateUniqueSuffix()
512
+ return `${truncatedName}-${uniqueSuffix}`
513
+ }
514
+
515
+ private generateUniqueSuffix(): string {
516
+ this.nameCounter += 1
517
+ return this.nameCounter.toString().padStart(4, '0')
518
+ }
519
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "sourceMap": true,
7
+ "outDir": "./build",
8
+ "target": "es2021",
9
+ "lib": ["es2022"],
10
+ "jsx": "react-jsx",
11
+ "module": "es2022",
12
+ "moduleResolution": "Bundler",
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "resolveJsonModule": true,
17
+ "allowJs": true,
18
+ "checkJs": false,
19
+ "isolatedModules": true,
20
+ "allowSyntheticDefaultImports": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "strict": true,
23
+ "skipLibCheck": true
24
+ },
25
+ "include": [ "test/**/*.ts", "scripts/**/*.ts", "src/**/*.ts", "examples/**/*"]
26
+ }