@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,70 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ import { OpenAPIV3 } from 'openapi-types'
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
+ import OpenAPISchemaValidator from 'openapi-schema-validator'
8
+
9
+ import { MCPProxy } from '../src/openapi-mcp-server/mcp/proxy'
10
+
11
+ export class ValidationError extends Error {
12
+ constructor(public errors: any[]) {
13
+ super('OpenAPI validation failed')
14
+ this.name = 'ValidationError'
15
+ }
16
+ }
17
+
18
+ export async function loadOpenApiSpec(specPath: string): Promise<OpenAPIV3.Document> {
19
+ let rawSpec: string
20
+
21
+ try {
22
+ rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
23
+ } catch (error) {
24
+ console.error('Failed to read OpenAPI specification file:', (error as Error).message)
25
+ process.exit(1)
26
+ }
27
+
28
+ // Parse and validate the spec
29
+ try {
30
+ const parsed = JSON.parse(rawSpec)
31
+ const baseUrl = process.env.BASE_URL
32
+
33
+ if (baseUrl) {
34
+ parsed.servers[0].url = baseUrl
35
+ }
36
+
37
+ return parsed as OpenAPIV3.Document
38
+ } catch (error) {
39
+ if (error instanceof ValidationError) {
40
+ throw error
41
+ }
42
+ console.error('Failed to parse OpenAPI specification:', (error as Error).message)
43
+ process.exit(1)
44
+ }
45
+ }
46
+
47
+ // Main execution
48
+ export async function main(args: string[] = process.argv.slice(2)) {
49
+ const filename = fileURLToPath(import.meta.url)
50
+ const directory = path.dirname(filename)
51
+ const specPath = path.resolve(directory, '../scripts/notion-openapi.json')
52
+ const openApiSpec = await loadOpenApiSpec(specPath)
53
+ const proxy = new MCPProxy('OpenAPI Tools', openApiSpec)
54
+
55
+ return proxy.connect(new StdioServerTransport())
56
+ }
57
+
58
+ const shouldStart = process.argv[1].endsWith('notion-mcp-server')
59
+ // Only run main if this is the entry point
60
+ if (shouldStart) {
61
+ main().catch(error => {
62
+ if (error instanceof ValidationError) {
63
+ console.error('Invalid OpenAPI 3.1 specification:')
64
+ error.errors.forEach(err => console.error(err))
65
+ } else {
66
+ console.error('Error:', error.message)
67
+ }
68
+ process.exit(1)
69
+ })
70
+ }
@@ -0,0 +1 @@
1
+ Note: This is a fork from v1 of https://github.com/snaggle-ai/openapi-mcp-server. The library took a different direction with v2 which is not compatible with our development approach.
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export * from './template'
@@ -0,0 +1,24 @@
1
+ import Mustache from 'mustache'
2
+ import { AuthTemplate, TemplateContext } from './types'
3
+
4
+ export function renderAuthTemplate(template: AuthTemplate, context: TemplateContext): AuthTemplate {
5
+ // Disable HTML escaping for URLs
6
+ Mustache.escape = (text) => text
7
+
8
+ // Render URL with template variables
9
+ const renderedUrl = Mustache.render(template.url, context)
10
+
11
+ // Create a new template object with rendered values
12
+ const renderedTemplate: AuthTemplate = {
13
+ ...template,
14
+ url: renderedUrl,
15
+ headers: { ...template.headers }, // Create a new headers object to avoid modifying the original
16
+ }
17
+
18
+ // Render body if it exists
19
+ if (template.body) {
20
+ renderedTemplate.body = Mustache.render(template.body, context)
21
+ }
22
+
23
+ return renderedTemplate
24
+ }
@@ -0,0 +1,26 @@
1
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
2
+
3
+ export interface AuthTemplate {
4
+ url: string
5
+ method: HttpMethod
6
+ headers: Record<string, string>
7
+ body?: string
8
+ }
9
+
10
+ export interface SecurityScheme {
11
+ [key: string]: {
12
+ tokenUrl?: string
13
+ [key: string]: any
14
+ }
15
+ }
16
+
17
+ export interface Server {
18
+ url: string
19
+ description?: string
20
+ }
21
+
22
+ export interface TemplateContext {
23
+ securityScheme?: SecurityScheme
24
+ servers?: Server[]
25
+ args: Record<string, string>
26
+ }
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { HttpClient } from '../http-client'
3
+ import { OpenAPIV3 } from 'openapi-types'
4
+ import fs from 'fs'
5
+ import FormData from 'form-data'
6
+
7
+ vi.mock('fs')
8
+ vi.mock('form-data')
9
+
10
+ describe('HttpClient File Upload', () => {
11
+ let client: HttpClient
12
+ const mockApiInstance = {
13
+ uploadFile: vi.fn(),
14
+ }
15
+
16
+ const baseConfig = {
17
+ baseUrl: 'http://test.com',
18
+ headers: {},
19
+ }
20
+
21
+ const mockOpenApiSpec: OpenAPIV3.Document = {
22
+ openapi: '3.0.0',
23
+ info: {
24
+ title: 'Test API',
25
+ version: '1.0.0',
26
+ },
27
+ paths: {
28
+ '/upload': {
29
+ post: {
30
+ operationId: 'uploadFile',
31
+ responses: {
32
+ '200': {
33
+ description: 'File uploaded successfully',
34
+ content: {
35
+ 'application/json': {
36
+ schema: {
37
+ type: 'object',
38
+ properties: {
39
+ success: {
40
+ type: 'boolean',
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ requestBody: {
49
+ content: {
50
+ 'multipart/form-data': {
51
+ schema: {
52
+ type: 'object',
53
+ properties: {
54
+ file: {
55
+ type: 'string',
56
+ format: 'binary',
57
+ },
58
+ description: {
59
+ type: 'string',
60
+ },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ }
70
+
71
+ beforeEach(() => {
72
+ vi.clearAllMocks()
73
+ client = new HttpClient(baseConfig, mockOpenApiSpec)
74
+ // @ts-expect-error - Mock the private api property
75
+ client['api'] = Promise.resolve(mockApiInstance)
76
+ })
77
+
78
+ it('should handle file uploads with FormData', async () => {
79
+ const mockFormData = new FormData()
80
+ const mockFileStream = { pipe: vi.fn() }
81
+ const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }
82
+
83
+ vi.mocked(fs.createReadStream).mockReturnValue(mockFileStream as any)
84
+ vi.mocked(FormData.prototype.append).mockImplementation(() => {})
85
+ vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)
86
+
87
+ const uploadPath = mockOpenApiSpec.paths['/upload']
88
+ if (!uploadPath?.post) {
89
+ throw new Error('Upload path not found in spec')
90
+ }
91
+ const operation = uploadPath.post as OpenAPIV3.OperationObject & { method: string; path: string }
92
+ const params = {
93
+ file: '/path/to/test.txt',
94
+ description: 'Test file',
95
+ }
96
+
97
+ mockApiInstance.uploadFile.mockResolvedValue({
98
+ data: { success: true },
99
+ status: 200,
100
+ headers: {},
101
+ })
102
+
103
+ await client.executeOperation(operation, params)
104
+
105
+ expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test.txt')
106
+ expect(FormData.prototype.append).toHaveBeenCalledWith('file', mockFileStream)
107
+ expect(FormData.prototype.append).toHaveBeenCalledWith('description', 'Test file')
108
+ expect(mockApiInstance.uploadFile).toHaveBeenCalledWith({}, expect.any(FormData), { headers: mockFormDataHeaders })
109
+ })
110
+
111
+ it('should throw error for invalid file path', async () => {
112
+ vi.mocked(fs.createReadStream).mockImplementation(() => {
113
+ throw new Error('File not found')
114
+ })
115
+
116
+ const uploadPath = mockOpenApiSpec.paths['/upload']
117
+ if (!uploadPath?.post) {
118
+ throw new Error('Upload path not found in spec')
119
+ }
120
+ const operation = uploadPath.post as OpenAPIV3.OperationObject & { method: string; path: string }
121
+ const params = {
122
+ file: '/nonexistent/file.txt',
123
+ description: 'Test file',
124
+ }
125
+
126
+ await expect(client.executeOperation(operation, params)).rejects.toThrow('Failed to read file at /nonexistent/file.txt')
127
+ })
128
+
129
+ it('should handle multiple file uploads', async () => {
130
+ const mockFormData = new FormData()
131
+ const mockFileStream1 = { pipe: vi.fn() }
132
+ const mockFileStream2 = { pipe: vi.fn() }
133
+ const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }
134
+
135
+ vi.mocked(fs.createReadStream)
136
+ .mockReturnValueOnce(mockFileStream1 as any)
137
+ .mockReturnValueOnce(mockFileStream2 as any)
138
+ vi.mocked(FormData.prototype.append).mockImplementation(() => {})
139
+ vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)
140
+
141
+ const operation: OpenAPIV3.OperationObject = {
142
+ operationId: 'uploadFile',
143
+ responses: {
144
+ '200': {
145
+ description: 'Files uploaded successfully',
146
+ content: {
147
+ 'application/json': {
148
+ schema: {
149
+ type: 'object',
150
+ properties: {
151
+ success: {
152
+ type: 'boolean',
153
+ },
154
+ },
155
+ },
156
+ },
157
+ },
158
+ },
159
+ },
160
+ requestBody: {
161
+ content: {
162
+ 'multipart/form-data': {
163
+ schema: {
164
+ type: 'object',
165
+ properties: {
166
+ file1: {
167
+ type: 'string',
168
+ format: 'binary',
169
+ },
170
+ file2: {
171
+ type: 'string',
172
+ format: 'binary',
173
+ },
174
+ description: {
175
+ type: 'string',
176
+ },
177
+ },
178
+ },
179
+ },
180
+ },
181
+ },
182
+ }
183
+
184
+ const params = {
185
+ file1: '/path/to/test1.txt',
186
+ file2: '/path/to/test2.txt',
187
+ description: 'Test files',
188
+ }
189
+
190
+ mockApiInstance.uploadFile.mockResolvedValue({
191
+ data: { success: true },
192
+ status: 200,
193
+ headers: {},
194
+ })
195
+
196
+ await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, params)
197
+
198
+ expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test1.txt')
199
+ expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test2.txt')
200
+ expect(FormData.prototype.append).toHaveBeenCalledWith('file1', mockFileStream1)
201
+ expect(FormData.prototype.append).toHaveBeenCalledWith('file2', mockFileStream2)
202
+ expect(FormData.prototype.append).toHaveBeenCalledWith('description', 'Test files')
203
+ expect(mockApiInstance.uploadFile).toHaveBeenCalledWith({}, expect.any(FormData), { headers: mockFormDataHeaders })
204
+ })
205
+ })
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
2
+ import { HttpClient } from '../http-client'
3
+ import type express from 'express'
4
+ //@ts-ignore
5
+ import { createPetstoreServer } from '../../../examples/petstore-server.cjs'
6
+ import type { OpenAPIV3 } from 'openapi-types'
7
+ import axios from 'axios'
8
+
9
+ interface Pet {
10
+ id: number
11
+ name: string
12
+ species: string
13
+ age: number
14
+ status: 'available' | 'pending' | 'sold'
15
+ }
16
+
17
+ describe('HttpClient Integration Tests', () => {
18
+ const PORT = 3456
19
+ const BASE_URL = `http://localhost:${PORT}`
20
+ let server: ReturnType<typeof express>
21
+ let openApiSpec: OpenAPIV3.Document
22
+ let client: HttpClient
23
+
24
+ beforeAll(async () => {
25
+ // Start the petstore server
26
+ server = createPetstoreServer(PORT) as unknown as express.Express
27
+
28
+ // Fetch the OpenAPI spec from the server
29
+ const response = await axios.get(`${BASE_URL}/openapi.json`)
30
+ openApiSpec = response.data
31
+
32
+ // Create HTTP client
33
+ client = new HttpClient(
34
+ {
35
+ baseUrl: BASE_URL,
36
+ headers: {
37
+ Accept: 'application/json',
38
+ },
39
+ },
40
+ openApiSpec,
41
+ )
42
+ })
43
+
44
+ afterAll(() => {
45
+ //@ts-expect-error
46
+ server.close()
47
+ })
48
+
49
+ it('should list all pets', async () => {
50
+ const operation = openApiSpec.paths['/pets']?.get
51
+ if (!operation) throw new Error('Operation not found')
52
+
53
+ const response = await client.executeOperation<Pet[]>(operation as OpenAPIV3.OperationObject & { method: string; path: string })
54
+
55
+ expect(response.status).toBe(200)
56
+ expect(Array.isArray(response.data)).toBe(true)
57
+ expect(response.data.length).toBeGreaterThan(0)
58
+ expect(response.data[0]).toHaveProperty('name')
59
+ expect(response.data[0]).toHaveProperty('species')
60
+ expect(response.data[0]).toHaveProperty('status')
61
+ })
62
+
63
+ it('should filter pets by status', async () => {
64
+ const operation = openApiSpec.paths['/pets']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
65
+ if (!operation) throw new Error('Operation not found')
66
+
67
+ const response = await client.executeOperation<Pet[]>(operation, { status: 'available' })
68
+
69
+ expect(response.status).toBe(200)
70
+ expect(Array.isArray(response.data)).toBe(true)
71
+ response.data.forEach((pet: Pet) => {
72
+ expect(pet.status).toBe('available')
73
+ })
74
+ })
75
+
76
+ it('should get a specific pet by ID', async () => {
77
+ const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
78
+ if (!operation) throw new Error('Operation not found')
79
+
80
+ const response = await client.executeOperation<Pet>(operation, { id: 1 })
81
+
82
+ expect(response.status).toBe(200)
83
+ expect(response.data).toHaveProperty('id', 1)
84
+ expect(response.data).toHaveProperty('name')
85
+ expect(response.data).toHaveProperty('species')
86
+ })
87
+
88
+ it('should create a new pet', async () => {
89
+ const operation = openApiSpec.paths['/pets']?.post as OpenAPIV3.OperationObject & { method: string; path: string }
90
+ if (!operation) throw new Error('Operation not found')
91
+
92
+ const newPet = {
93
+ name: 'TestPet',
94
+ species: 'Dog',
95
+ age: 2,
96
+ }
97
+
98
+ const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, newPet)
99
+
100
+ expect(response.status).toBe(201)
101
+ expect(response.data).toMatchObject({
102
+ ...newPet,
103
+ status: 'available',
104
+ })
105
+ expect(response.data.id).toBeDefined()
106
+ })
107
+
108
+ it("should update a pet's status", async () => {
109
+ const operation = openApiSpec.paths['/pets/{id}']?.put
110
+ if (!operation) throw new Error('Operation not found')
111
+
112
+ const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {
113
+ id: 1,
114
+ status: 'sold',
115
+ })
116
+
117
+ expect(response.status).toBe(200)
118
+ expect(response.data).toHaveProperty('id', 1)
119
+ expect(response.data).toHaveProperty('status', 'sold')
120
+ })
121
+
122
+ it('should delete a pet', async () => {
123
+ // First create a pet to delete
124
+ const createOperation = openApiSpec.paths['/pets']?.post
125
+ if (!createOperation) throw new Error('Operation not found')
126
+
127
+ const createResponse = await client.executeOperation<Pet>(
128
+ createOperation as OpenAPIV3.OperationObject & { method: string; path: string },
129
+ {
130
+ name: 'ToDelete',
131
+ species: 'Cat',
132
+ age: 3,
133
+ },
134
+ )
135
+ const petId = createResponse.data.id
136
+
137
+ // Then delete it
138
+ const deleteOperation = openApiSpec.paths['/pets/{id}']?.delete
139
+ if (!deleteOperation) throw new Error('Operation not found')
140
+
141
+ const deleteResponse = await client.executeOperation(deleteOperation as OpenAPIV3.OperationObject & { method: string; path: string }, {
142
+ id: petId,
143
+ })
144
+
145
+ expect(deleteResponse.status).toBe(204)
146
+
147
+ // Verify the pet is deleted
148
+ const getOperation = openApiSpec.paths['/pets/{id}']?.get
149
+ if (!getOperation) throw new Error('Operation not found')
150
+
151
+ try {
152
+ await client.executeOperation(getOperation as OpenAPIV3.OperationObject & { method: string; path: string }, { id: petId })
153
+ throw new Error('Should not reach here')
154
+ } catch (error: any) {
155
+ expect(error.message).toContain('404')
156
+ }
157
+ })
158
+
159
+ it('should handle errors appropriately', async () => {
160
+ const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
161
+ if (!operation) throw new Error('Operation not found')
162
+
163
+ try {
164
+ await client.executeOperation(
165
+ operation as OpenAPIV3.OperationObject & { method: string; path: string },
166
+ { id: 99999 }, // Non-existent ID
167
+ )
168
+ throw new Error('Should not reach here')
169
+ } catch (error: any) {
170
+ expect(error.message).toContain('404')
171
+ }
172
+ })
173
+ })