@ainote/mcp 1.0.2 → 1.0.3
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/README.md +107 -2
- package/index.js +17 -221
- package/lib/auth/mcp-key.js +14 -0
- package/lib/auth/oauth.js +18 -0
- package/lib/auth/strategy-factory.js +18 -0
- package/lib/core/api-client.js +1 -0
- package/lib/core/config-manager.js +67 -0
- package/lib/core/config.js +1 -0
- package/lib/core/mcp-server.js +2 -0
- package/lib/core/server-factory.js +77 -0
- package/lib/core/tool-registry.js +31 -0
- package/lib/tools/chatgpt-tools.js +93 -0
- package/lib/tools/index.js +3 -0
- package/lib/tools/internal/formatters.js +37 -0
- package/lib/tools/shared-tools.js +152 -0
- package/lib/utils/api-client.js +74 -0
- package/package.json +40 -5
- package/server.js +103 -0
package/README.md
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/%40ainote%2Fmcp-server)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
A Model Context Protocol (MCP) server that connects
|
|
6
|
+
A Model Context Protocol (MCP) server that connects AI assistants to your AI Note task management system. This package enables direct interaction with your AI Note tasks through natural language conversations in Claude Desktop and other MCP-compatible platforms.
|
|
7
|
+
|
|
8
|
+
> 📚 **[Complete Project Guide](../docs/PROJECT_GUIDE.md)** - 전체 프로젝트 가이드 및 아키텍처 정보
|
|
7
9
|
|
|
8
10
|
## Table of Contents
|
|
9
11
|
|
|
12
|
+
- [Access Methods](#access-methods)
|
|
10
13
|
- [Features](#features)
|
|
11
14
|
- [Prerequisites](#prerequisites)
|
|
12
15
|
- [Installation](#installation)
|
|
13
16
|
- [Configuration](#configuration)
|
|
14
17
|
- [Usage](#usage)
|
|
18
|
+
- [HTTP Endpoint](#http-endpoint)
|
|
15
19
|
- [Available Tools](#available-tools)
|
|
16
20
|
- [API Reference](#api-reference)
|
|
17
21
|
- [Development](#development)
|
|
@@ -19,6 +23,32 @@ A Model Context Protocol (MCP) server that connects Claude Desktop to your AI No
|
|
|
19
23
|
- [Contributing](#contributing)
|
|
20
24
|
- [License](#license)
|
|
21
25
|
|
|
26
|
+
## Access Methods
|
|
27
|
+
|
|
28
|
+
AI Note MCP server is available through three transport options to cover different integration scenarios:
|
|
29
|
+
|
|
30
|
+
### 1. **stdio Mode (This Package)** - For Personal Use
|
|
31
|
+
- **Installation**: `npm install -g @ainote/mcp`
|
|
32
|
+
- **Usage**: Claude Desktop and other stdio-based MCP clients
|
|
33
|
+
- **Setup**: Local installation with API key configuration
|
|
34
|
+
- **Best for**: Individual users connecting their personal AI Note account to Claude Desktop
|
|
35
|
+
|
|
36
|
+
### 2. **Local SSE Bridge (ChatGPT / MCP Apps)**
|
|
37
|
+
- **Command**: `ainote-mcp-http`
|
|
38
|
+
- **Protocol**: Server-Sent Events (SSE) + JSON-RPC over HTTP POST
|
|
39
|
+
- **Usage**: ChatGPT Model Context Protocol connectors, other SSE-capable MCP clients
|
|
40
|
+
- **Setup**: Run locally alongside your browser; supports API key or (optional) OAuth bearer tokens
|
|
41
|
+
- **Best for**: Users wanting to expose AI Note tools to ChatGPT without deploying infrastructure
|
|
42
|
+
|
|
43
|
+
### 3. **Hosted HTTP Endpoint** - For Platform Integration
|
|
44
|
+
- **URL**: `https://api.ainote.dev/mcp`
|
|
45
|
+
- **Protocol**: JSON-RPC 2.0 over HTTP
|
|
46
|
+
- **Usage**: Platform integrations (Kakao PlayMCP, etc.)
|
|
47
|
+
- **Setup**: No installation required, direct API access
|
|
48
|
+
- **Best for**: Third-party platforms and services integrating AI Note functionality
|
|
49
|
+
|
|
50
|
+
All transports expose the same tool catalog. Choose the option that matches your hosting model and client capabilities.
|
|
51
|
+
|
|
22
52
|
## Features
|
|
23
53
|
|
|
24
54
|
- 📝 **Task Management**: Create, update, delete, and list tasks directly from Claude Desktop
|
|
@@ -30,7 +60,7 @@ A Model Context Protocol (MCP) server that connects Claude Desktop to your AI No
|
|
|
30
60
|
|
|
31
61
|
## Prerequisites
|
|
32
62
|
|
|
33
|
-
- Node.js >=
|
|
63
|
+
- Node.js >= 18.0.0
|
|
34
64
|
- npm or yarn
|
|
35
65
|
- AI Note API access (API key required)
|
|
36
66
|
- Claude Desktop with MCP support enabled
|
|
@@ -139,6 +169,81 @@ You: "Mark task ID 123 as completed"
|
|
|
139
169
|
Claude: I'll mark that task as completed...
|
|
140
170
|
```
|
|
141
171
|
|
|
172
|
+
## HTTP Endpoint
|
|
173
|
+
|
|
174
|
+
For platform integrations and services that prefer HTTP over stdio, AI Note provides a direct HTTP endpoint:
|
|
175
|
+
|
|
176
|
+
### Endpoint URL
|
|
177
|
+
```
|
|
178
|
+
POST https://api.ainote.dev/mcp
|
|
179
|
+
Content-Type: application/json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Authentication
|
|
183
|
+
```http
|
|
184
|
+
Authorization: Bearer YOUR_API_KEY
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Request Format (JSON-RPC 2.0)
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"jsonrpc": "2.0",
|
|
191
|
+
"method": "tools/list",
|
|
192
|
+
"id": 1
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Response Format
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"jsonrpc": "2.0",
|
|
200
|
+
"result": {
|
|
201
|
+
"tools": [...]
|
|
202
|
+
},
|
|
203
|
+
"id": 1
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Supported Methods
|
|
208
|
+
- `ping` - Health check
|
|
209
|
+
- `capabilities` - Server capabilities
|
|
210
|
+
- `tools/list` - List available tools
|
|
211
|
+
- `tools/call` - Execute a specific tool
|
|
212
|
+
|
|
213
|
+
### Platform Integration Examples
|
|
214
|
+
- **Kakao PlayMCP**: Use the HTTP endpoint for seamless integration
|
|
215
|
+
- **Custom Applications**: Build your own MCP client using the HTTP API
|
|
216
|
+
- **Enterprise Solutions**: Direct server-to-server communication
|
|
217
|
+
|
|
218
|
+
This HTTP endpoint provides identical functionality to the stdio version, making AI Note MCP accessible to a broader range of platforms and integration scenarios.
|
|
219
|
+
|
|
220
|
+
### Local Development Bridge (`ainote-mcp-http`)
|
|
221
|
+
|
|
222
|
+
Run the bundled HTTP/SSE bridge when you need a local endpoint for ChatGPT or other MCP clients that speak SSE:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
ainote-mcp-http
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
By default the server listens on `http://localhost:3030` and exposes two endpoints:
|
|
229
|
+
|
|
230
|
+
- `GET /sse` – establishes the SSE stream and returns the `sessionId`
|
|
231
|
+
- `POST /messages?sessionId=...` – receives JSON-RPC payloads from the client
|
|
232
|
+
- `GET /health` – simple health probe for monitoring
|
|
233
|
+
|
|
234
|
+
#### Configuration
|
|
235
|
+
|
|
236
|
+
Environment variable | Description | Default
|
|
237
|
+
---|---|---
|
|
238
|
+
`AINOTE_API_KEY` | API key used for MCP key authentication | **required**
|
|
239
|
+
`AINOTE_API_URL` | Target AI Note API base URL | `https://ainote-5muq.onrender.com`
|
|
240
|
+
`AINOTE_MCP_HTTP_PORT` | Local port for the SSE server | `3030`
|
|
241
|
+
`AINOTE_MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed browser origins | _any_
|
|
242
|
+
`AINOTE_MCP_ALLOWED_HOSTS` | Comma-separated list of allowed `Host` headers | _any_
|
|
243
|
+
`AINOTE_ENABLE_OAUTH_AUTH` | Set to `true` to allow Bearer tokens (OAuth) | `false`
|
|
244
|
+
|
|
245
|
+
When OAuth is enabled the server expects `Authorization: Bearer ...` headers from the client and forwards them to the AI Note API.
|
|
246
|
+
|
|
142
247
|
## Available Tools
|
|
143
248
|
|
|
144
249
|
### list_tasks
|
package/index.js
CHANGED
|
@@ -1,229 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import axios from 'axios';
|
|
6
|
-
|
|
7
|
-
const API_KEY = process.env.AINOTE_API_KEY;
|
|
8
|
-
const API_URL = process.env.AINOTE_API_URL || 'https://api.ainote.dev';
|
|
9
|
-
|
|
10
|
-
// Create axios instance with default config
|
|
11
|
-
const api = axios.create({
|
|
12
|
-
baseURL: API_URL,
|
|
13
|
-
headers: {
|
|
14
|
-
'Authorization': `McpKey ${API_KEY}`,
|
|
15
|
-
'Content-Type': 'application/json'
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// Create MCP server
|
|
20
|
-
const server = new Server(
|
|
21
|
-
{
|
|
22
|
-
name: 'ainote-mcp',
|
|
23
|
-
version: '1.0.0',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
capabilities: {
|
|
27
|
-
tools: {},
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
// Define available tools
|
|
33
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
34
|
-
return {
|
|
35
|
-
tools: [
|
|
36
|
-
{
|
|
37
|
-
name: 'list_tasks',
|
|
38
|
-
description: 'List tasks from AI Note',
|
|
39
|
-
inputSchema: {
|
|
40
|
-
type: 'object',
|
|
41
|
-
properties: {
|
|
42
|
-
status: {
|
|
43
|
-
type: 'string',
|
|
44
|
-
enum: ['pending', 'completed'],
|
|
45
|
-
description: 'Filter by task status'
|
|
46
|
-
},
|
|
47
|
-
limit: {
|
|
48
|
-
type: 'number',
|
|
49
|
-
description: 'Maximum number of tasks to return (default: 25, max: 500)'
|
|
50
|
-
},
|
|
51
|
-
search: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
description: 'Search keyword in task content'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'create_task',
|
|
60
|
-
description: 'Create a new task in AI Note',
|
|
61
|
-
inputSchema: {
|
|
62
|
-
type: 'object',
|
|
63
|
-
properties: {
|
|
64
|
-
content: {
|
|
65
|
-
type: 'string',
|
|
66
|
-
description: 'Task content'
|
|
67
|
-
},
|
|
68
|
-
is_important: {
|
|
69
|
-
type: 'boolean',
|
|
70
|
-
description: 'Mark task as important'
|
|
71
|
-
},
|
|
72
|
-
due_date: {
|
|
73
|
-
type: 'string',
|
|
74
|
-
description: 'Due date in ISO format'
|
|
75
|
-
},
|
|
76
|
-
category_id: {
|
|
77
|
-
type: 'string',
|
|
78
|
-
description: 'Category ID'
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
required: ['content']
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: 'update_task',
|
|
86
|
-
description: 'Update an existing task',
|
|
87
|
-
inputSchema: {
|
|
88
|
-
type: 'object',
|
|
89
|
-
properties: {
|
|
90
|
-
id: {
|
|
91
|
-
type: 'string',
|
|
92
|
-
description: 'Task ID'
|
|
93
|
-
},
|
|
94
|
-
content: {
|
|
95
|
-
type: 'string',
|
|
96
|
-
description: 'New task content'
|
|
97
|
-
},
|
|
98
|
-
is_important: {
|
|
99
|
-
type: 'boolean',
|
|
100
|
-
description: 'Update important status'
|
|
101
|
-
},
|
|
102
|
-
completed_at: {
|
|
103
|
-
type: 'string',
|
|
104
|
-
description: 'Mark as completed (ISO format) or null to uncomplete'
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
required: ['id']
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: 'delete_task',
|
|
112
|
-
description: 'Delete a task (soft delete)',
|
|
113
|
-
inputSchema: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
id: {
|
|
117
|
-
type: 'string',
|
|
118
|
-
description: 'Task ID to delete'
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
required: ['id']
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: 'list_categories',
|
|
126
|
-
description: 'List all categories',
|
|
127
|
-
inputSchema: {
|
|
128
|
-
type: 'object',
|
|
129
|
-
properties: {}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
};
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Handle tool calls
|
|
137
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
138
|
-
const { name, arguments: args } = request.params;
|
|
2
|
+
/**
|
|
3
|
+
* AI Note MCP Server - Claude Desktop Integration (stdio transport)
|
|
4
|
+
*/
|
|
139
5
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const response = await api.get('/api/mcp/tasks', {
|
|
144
|
-
params: args
|
|
145
|
-
});
|
|
146
|
-
return {
|
|
147
|
-
content: [
|
|
148
|
-
{
|
|
149
|
-
type: 'text',
|
|
150
|
-
text: JSON.stringify(response.data, null, 2)
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
case 'create_task': {
|
|
157
|
-
const response = await api.post('/api/mcp/tasks', {
|
|
158
|
-
task: args
|
|
159
|
-
});
|
|
160
|
-
return {
|
|
161
|
-
content: [
|
|
162
|
-
{
|
|
163
|
-
type: 'text',
|
|
164
|
-
text: JSON.stringify(response.data, null, 2)
|
|
165
|
-
}
|
|
166
|
-
]
|
|
167
|
-
};
|
|
168
|
-
}
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { resolveAuthContext } from './lib/auth/strategy-factory.js';
|
|
8
|
+
import { createClaudeServer } from './lib/core/server-factory.js';
|
|
169
9
|
|
|
170
|
-
|
|
171
|
-
const { id, ...updateData } = args;
|
|
172
|
-
const response = await api.put(`/api/mcp/tasks/${id}`, {
|
|
173
|
-
task: updateData
|
|
174
|
-
});
|
|
175
|
-
return {
|
|
176
|
-
content: [
|
|
177
|
-
{
|
|
178
|
-
type: 'text',
|
|
179
|
-
text: JSON.stringify(response.data, null, 2)
|
|
180
|
-
}
|
|
181
|
-
]
|
|
182
|
-
};
|
|
183
|
-
}
|
|
10
|
+
const transport = new StdioServerTransport();
|
|
184
11
|
|
|
185
|
-
|
|
186
|
-
const response = await api.delete(`/api/mcp/tasks/${args.id}`);
|
|
187
|
-
return {
|
|
188
|
-
content: [
|
|
189
|
-
{
|
|
190
|
-
type: 'text',
|
|
191
|
-
text: JSON.stringify(response.data, null, 2)
|
|
192
|
-
}
|
|
193
|
-
]
|
|
194
|
-
};
|
|
195
|
-
}
|
|
12
|
+
let defaultAuthContext;
|
|
196
13
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
]
|
|
206
|
-
};
|
|
207
|
-
}
|
|
14
|
+
try {
|
|
15
|
+
defaultAuthContext = resolveAuthContext('stdio');
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(`[ainote-mcp] Authentication error: ${error.message}`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
208
21
|
|
|
209
|
-
|
|
210
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
return {
|
|
214
|
-
content: [
|
|
215
|
-
{
|
|
216
|
-
type: 'text',
|
|
217
|
-
text: `Error: ${error.message}\n${error.response?.data ? JSON.stringify(error.response.data, null, 2) : ''}`
|
|
218
|
-
}
|
|
219
|
-
],
|
|
220
|
-
isError: true
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
});
|
|
22
|
+
const { server } = createClaudeServer({ defaultAuthContext });
|
|
224
23
|
|
|
225
|
-
// Start the server
|
|
226
|
-
const transport = new StdioServerTransport();
|
|
227
24
|
await server.connect(transport);
|
|
228
|
-
|
|
229
|
-
console.error('AI Note MCP proxy server started');
|
|
25
|
+
console.error('AI Note MCP stdio server started (Claude Desktop compatible)');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getConfig } from '../core/config.js';
|
|
2
|
+
|
|
3
|
+
export function authenticateWithMcpKey() {
|
|
4
|
+
const config = getConfig();
|
|
5
|
+
|
|
6
|
+
if (!config.api.key) {
|
|
7
|
+
throw new Error('AINOTE_API_KEY is required to authenticate MCP requests');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
type: 'mcpKey',
|
|
12
|
+
apiKey: config.api.key
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function authenticateWithOAuth(headers = {}) {
|
|
2
|
+
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
3
|
+
|
|
4
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
5
|
+
throw new Error('Missing or invalid Bearer token for OAuth authentication');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const token = authHeader.substring('Bearer '.length).trim();
|
|
9
|
+
|
|
10
|
+
if (!token) {
|
|
11
|
+
throw new Error('Bearer token is empty');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
type: 'oauth',
|
|
16
|
+
token
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { authenticateWithMcpKey } from './mcp-key.js';
|
|
2
|
+
import { authenticateWithOAuth } from './oauth.js';
|
|
3
|
+
import { getFeatureFlags } from '../core/config.js';
|
|
4
|
+
|
|
5
|
+
export function resolveAuthContext(transportType, headers = {}) {
|
|
6
|
+
if (transportType === 'stdio') {
|
|
7
|
+
return authenticateWithMcpKey();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const features = getFeatureFlags();
|
|
11
|
+
|
|
12
|
+
if (transportType === 'sse' && features.oauthAuth) {
|
|
13
|
+
return authenticateWithOAuth(headers);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Default fallback to MCP key to maintain backward compatibility.
|
|
17
|
+
return authenticateWithMcpKey();
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../utils/api-client.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
|
|
2
|
+
const DEFAULT_HTTP_PORT = 3030;
|
|
3
|
+
|
|
4
|
+
const parseBoolean = (value, fallback) => {
|
|
5
|
+
if (value === undefined) {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === 'boolean') {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
const normalized = `${value}`.trim().toLowerCase();
|
|
12
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const parseNumber = (value, fallback) => {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
const parsed = Number(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const parseList = (value) => {
|
|
30
|
+
if (!value) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((item) => item.trim())
|
|
39
|
+
.filter((item) => item.length > 0);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function getConfig() {
|
|
43
|
+
const features = {
|
|
44
|
+
chatgptSupport: parseBoolean(process.env.AINOTE_ENABLE_CHATGPT_SUPPORT, true),
|
|
45
|
+
oauthAuth: parseBoolean(process.env.AINOTE_ENABLE_OAUTH_AUTH, false),
|
|
46
|
+
sseTransport: parseBoolean(process.env.AINOTE_ENABLE_SSE_TRANSPORT, true)
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
api: {
|
|
51
|
+
key: process.env.AINOTE_API_KEY,
|
|
52
|
+
url: process.env.AINOTE_API_URL || DEFAULT_API_URL
|
|
53
|
+
},
|
|
54
|
+
http: {
|
|
55
|
+
port: parseNumber(process.env.AINOTE_MCP_HTTP_PORT, DEFAULT_HTTP_PORT),
|
|
56
|
+
endpoint: process.env.AINOTE_MCP_HTTP_ENDPOINT || '/sse',
|
|
57
|
+
messageEndpoint: process.env.AINOTE_MCP_HTTP_MESSAGES_ENDPOINT || '/messages',
|
|
58
|
+
allowedOrigins: parseList(process.env.AINOTE_MCP_ALLOWED_ORIGINS),
|
|
59
|
+
allowedHosts: parseList(process.env.AINOTE_MCP_ALLOWED_HOSTS)
|
|
60
|
+
},
|
|
61
|
+
features
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getFeatureFlags() {
|
|
66
|
+
return getConfig().features;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './config-manager.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { ToolRegistry } from './tool-registry.js';
|
|
4
|
+
import { getConfig } from './config-manager.js';
|
|
5
|
+
import { createApiClient } from '../utils/api-client.js';
|
|
6
|
+
import { getSharedTools } from '../tools/shared-tools.js';
|
|
7
|
+
import { getChatGptTools } from '../tools/chatgpt-tools.js';
|
|
8
|
+
import { toErrorContent } from '../tools/internal/formatters.js';
|
|
9
|
+
|
|
10
|
+
function buildServerInfo(profile) {
|
|
11
|
+
return {
|
|
12
|
+
name: `ainote-mcp-${profile}`,
|
|
13
|
+
version: '1.1.0'
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function registerTools(registry, { includeChatGpt }) {
|
|
18
|
+
registry.registerMany(getSharedTools());
|
|
19
|
+
|
|
20
|
+
if (includeChatGpt) {
|
|
21
|
+
registry.registerMany(getChatGptTools());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createServer({ profile, defaultAuthContext } = {}) {
|
|
26
|
+
const config = getConfig();
|
|
27
|
+
const includeChatGpt = profile === 'chatgpt' && config.features.chatgptSupport;
|
|
28
|
+
const registry = new ToolRegistry();
|
|
29
|
+
|
|
30
|
+
registerTools(registry, { includeChatGpt });
|
|
31
|
+
|
|
32
|
+
const server = new Server(buildServerInfo(profile), {
|
|
33
|
+
capabilities: {
|
|
34
|
+
tools: {}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
39
|
+
tools: registry.listDefinitions()
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
43
|
+
try {
|
|
44
|
+
const { name, arguments: args } = request.params;
|
|
45
|
+
console.error(`[MCP] Calling tool: ${name}`, JSON.stringify(args));
|
|
46
|
+
|
|
47
|
+
const authContext = extra?.authInfo ?? defaultAuthContext;
|
|
48
|
+
const apiClient = createApiClient(authContext);
|
|
49
|
+
|
|
50
|
+
const result = await registry.execute(name, args, {
|
|
51
|
+
apiClient,
|
|
52
|
+
request,
|
|
53
|
+
extra
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.error(`[MCP] Tool ${name} completed successfully`);
|
|
57
|
+
return result;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`[MCP] Tool error:`, error);
|
|
60
|
+
return toErrorContent(error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { server, registry };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createClaudeServer(options = {}) {
|
|
68
|
+
return createServer({ profile: 'claude', ...options });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createChatGptServer(options = {}) {
|
|
72
|
+
return createServer({ profile: 'chatgpt', ...options });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createServerForProfile(profile, options = {}) {
|
|
76
|
+
return createServer({ profile, ...options });
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class ToolRegistry {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.tools = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
register(tool) {
|
|
7
|
+
if (!tool?.definition?.name) {
|
|
8
|
+
throw new Error('Invalid tool registration: missing tool name');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.tools.set(tool.definition.name, tool);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerMany(tools = []) {
|
|
15
|
+
tools.forEach((tool) => this.register(tool));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
listDefinitions() {
|
|
19
|
+
return Array.from(this.tools.values()).map((tool) => tool.definition);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async execute(name, args, context) {
|
|
23
|
+
const tool = this.tools.get(name);
|
|
24
|
+
|
|
25
|
+
if (!tool) {
|
|
26
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return tool.handler(args ?? {}, context ?? {});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { toSuccessContent } from './internal/formatters.js';
|
|
2
|
+
|
|
3
|
+
const MAX_RESULTS = 20;
|
|
4
|
+
|
|
5
|
+
function searchDefinition() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'search',
|
|
8
|
+
description: 'Search tasks and categories in AI Note',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
query: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Search query for tasks content, categories, or due dates'
|
|
15
|
+
},
|
|
16
|
+
limit: {
|
|
17
|
+
type: 'number',
|
|
18
|
+
description: 'Maximum number of search results (default: 20)'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ['query']
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fetchDefinition() {
|
|
27
|
+
return {
|
|
28
|
+
name: 'fetch',
|
|
29
|
+
description: 'Fetch detailed information about a specific task',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
id: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Task ID to fetch detailed information'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
required: ['id']
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getChatGptTools() {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
definition: searchDefinition(),
|
|
47
|
+
handler: async (args, { apiClient }) => {
|
|
48
|
+
const limit = typeof args.limit === 'number' ? Math.min(args.limit, MAX_RESULTS) : MAX_RESULTS;
|
|
49
|
+
const response = await apiClient.get('/api/mcp/tasks', {
|
|
50
|
+
params: {
|
|
51
|
+
search: args.query,
|
|
52
|
+
limit
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const results = Array.isArray(response.data?.tasks)
|
|
57
|
+
? response.data.tasks.map((task) => ({
|
|
58
|
+
id: task.id,
|
|
59
|
+
title: task.content,
|
|
60
|
+
due_date: task.due_date,
|
|
61
|
+
is_important: task.is_important,
|
|
62
|
+
url: task.url ?? `https://app.ainote.dev/tasks/${task.id}`,
|
|
63
|
+
category: task.category
|
|
64
|
+
}))
|
|
65
|
+
: response.data;
|
|
66
|
+
|
|
67
|
+
return toSuccessContent({
|
|
68
|
+
results,
|
|
69
|
+
meta: {
|
|
70
|
+
source: 'ai-note',
|
|
71
|
+
query: args.query,
|
|
72
|
+
limit
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
definition: fetchDefinition(),
|
|
79
|
+
handler: async (args, { apiClient }) => {
|
|
80
|
+
const response = await apiClient.get(`/api/mcp/tasks/${args.id}`);
|
|
81
|
+
const task = response.data?.task ?? response.data;
|
|
82
|
+
|
|
83
|
+
return toSuccessContent({
|
|
84
|
+
task,
|
|
85
|
+
meta: {
|
|
86
|
+
source: 'ai-note',
|
|
87
|
+
fetched_at: new Date().toISOString()
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function toSuccessContent(payload, options = {}) {
|
|
2
|
+
const { pretty = true } = options;
|
|
3
|
+
const text = pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
content: [
|
|
7
|
+
{
|
|
8
|
+
type: 'text',
|
|
9
|
+
text
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function toErrorContent(error) {
|
|
16
|
+
const message = error?.message ?? String(error);
|
|
17
|
+
const details = error?.response?.data;
|
|
18
|
+
const textParts = [`Error: ${message}`];
|
|
19
|
+
|
|
20
|
+
if (details) {
|
|
21
|
+
try {
|
|
22
|
+
textParts.push(JSON.stringify(details, null, 2));
|
|
23
|
+
} catch (stringifyError) {
|
|
24
|
+
textParts.push(String(details));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: textParts.join('\n')
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
isError: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { toSuccessContent } from './internal/formatters.js';
|
|
2
|
+
|
|
3
|
+
function listTasksDefinition() {
|
|
4
|
+
return {
|
|
5
|
+
name: 'list_tasks',
|
|
6
|
+
description: 'List tasks from AI Note',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
status: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
enum: ['pending', 'completed'],
|
|
13
|
+
description: 'Filter by task status'
|
|
14
|
+
},
|
|
15
|
+
limit: {
|
|
16
|
+
type: 'number',
|
|
17
|
+
description: 'Maximum number of tasks to return (default: 25, max: 500)'
|
|
18
|
+
},
|
|
19
|
+
search: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'Search keyword in task content'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createTaskDefinition() {
|
|
29
|
+
return {
|
|
30
|
+
name: 'create_task',
|
|
31
|
+
description: 'Create a new task in AI Note',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
content: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Task content'
|
|
38
|
+
},
|
|
39
|
+
is_important: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'Mark task as important'
|
|
42
|
+
},
|
|
43
|
+
due_date: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Due date in ISO format'
|
|
46
|
+
},
|
|
47
|
+
category_id: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Category ID'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required: ['content']
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateTaskDefinition() {
|
|
58
|
+
return {
|
|
59
|
+
name: 'update_task',
|
|
60
|
+
description: 'Update an existing task',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
id: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Task ID'
|
|
67
|
+
},
|
|
68
|
+
content: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'New task content'
|
|
71
|
+
},
|
|
72
|
+
is_important: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'Update important status'
|
|
75
|
+
},
|
|
76
|
+
completed_at: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Mark as completed (ISO format) or null to uncomplete'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
required: ['id']
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deleteTaskDefinition() {
|
|
87
|
+
return {
|
|
88
|
+
name: 'delete_task',
|
|
89
|
+
description: 'Delete a task (soft delete)',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
id: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Task ID to delete'
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
required: ['id']
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function listCategoriesDefinition() {
|
|
104
|
+
return {
|
|
105
|
+
name: 'list_categories',
|
|
106
|
+
description: 'List all categories',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getSharedTools() {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
definition: listTasksDefinition(),
|
|
118
|
+
handler: async (args, { apiClient }) => {
|
|
119
|
+
const result = await apiClient.callTool('list_tasks', args);
|
|
120
|
+
return result; // Return full result with { content: [...] }
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
definition: createTaskDefinition(),
|
|
125
|
+
handler: async (args, { apiClient }) => {
|
|
126
|
+
const result = await apiClient.callTool('create_task', args);
|
|
127
|
+
return result; // Return full result with { content: [...] }
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
definition: updateTaskDefinition(),
|
|
132
|
+
handler: async (args, { apiClient }) => {
|
|
133
|
+
const result = await apiClient.callTool('update_task', args);
|
|
134
|
+
return result; // Return full result with { content: [...] }
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
definition: deleteTaskDefinition(),
|
|
139
|
+
handler: async (args, { apiClient }) => {
|
|
140
|
+
const result = await apiClient.callTool('delete_task', args);
|
|
141
|
+
return result; // Return full result with { content: [...] }
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
definition: listCategoriesDefinition(),
|
|
146
|
+
handler: async (_args, { apiClient }) => {
|
|
147
|
+
const result = await apiClient.callTool('list_categories', {});
|
|
148
|
+
return result; // Return full result with { content: [...] }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { getConfig } from '../core/config-manager.js';
|
|
3
|
+
|
|
4
|
+
function buildHeaders(authContext, config) {
|
|
5
|
+
const headers = {
|
|
6
|
+
'Content-Type': 'application/json'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const contextType = authContext?.type ?? 'mcpKey';
|
|
10
|
+
|
|
11
|
+
if (contextType === 'oauth') {
|
|
12
|
+
const bearerToken = authContext?.token;
|
|
13
|
+
if (!bearerToken) {
|
|
14
|
+
throw new Error('OAuth authentication requires a bearer token');
|
|
15
|
+
}
|
|
16
|
+
headers.Authorization = `Bearer ${bearerToken}`;
|
|
17
|
+
} else {
|
|
18
|
+
const apiKey = authContext?.apiKey ?? config.api.key;
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new Error('AINOTE_API_KEY environment variable is required for MCP authentication');
|
|
21
|
+
}
|
|
22
|
+
headers.Authorization = `McpKey ${apiKey}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return headers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let requestIdCounter = 1;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* JSON-RPC 2.0 요청 생성 헬퍼
|
|
32
|
+
*/
|
|
33
|
+
function createJsonRpcRequest(method, params = {}) {
|
|
34
|
+
return {
|
|
35
|
+
jsonrpc: '2.0',
|
|
36
|
+
id: requestIdCounter++,
|
|
37
|
+
method,
|
|
38
|
+
params
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* JSON-RPC 2.0 tools/call 요청 생성
|
|
44
|
+
*/
|
|
45
|
+
function createToolCallRequest(toolName, arguments_) {
|
|
46
|
+
return createJsonRpcRequest('tools/call', {
|
|
47
|
+
name: toolName,
|
|
48
|
+
arguments: arguments_
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createApiClient(authContext) {
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
const headers = buildHeaders(authContext, config);
|
|
55
|
+
|
|
56
|
+
const axiosInstance = axios.create({
|
|
57
|
+
baseURL: config.api.url,
|
|
58
|
+
headers
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// JSON-RPC helper 메서드 추가
|
|
62
|
+
axiosInstance.callTool = async (toolName, arguments_) => {
|
|
63
|
+
const rpcRequest = createToolCallRequest(toolName, arguments_);
|
|
64
|
+
const response = await axiosInstance.post('/api/mcp', rpcRequest);
|
|
65
|
+
|
|
66
|
+
if (response.data.error) {
|
|
67
|
+
throw new Error(`JSON-RPC Error [${response.data.error.code}]: ${response.data.error.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return response.data.result;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return axiosInstance;
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainote/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MCP server for AI Note - Connect Claude Desktop to your AI Note tasks",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"ainote-mcp": "./index.js"
|
|
8
|
+
"ainote-mcp": "./index.js",
|
|
9
|
+
"ainote-mcp-http": "./server.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"start": "node index.js",
|
|
12
|
-
"
|
|
13
|
+
"start:http": "node server.js",
|
|
14
|
+
"test": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest",
|
|
15
|
+
"test:watch": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
16
|
+
"test:coverage": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
17
|
+
"test:verbose": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --verbose"
|
|
13
18
|
},
|
|
14
19
|
"keywords": [
|
|
15
20
|
"mcp",
|
|
@@ -37,14 +42,44 @@
|
|
|
37
42
|
},
|
|
38
43
|
"homepage": "https://github.com/ainote-dev/ainote-mcp#readme",
|
|
39
44
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
45
|
+
"node": ">=18.0.0"
|
|
41
46
|
},
|
|
42
47
|
"dependencies": {
|
|
43
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
|
-
"axios": "^1.6.0"
|
|
49
|
+
"axios": "^1.6.0",
|
|
50
|
+
"cors": "^2.8.5",
|
|
51
|
+
"express": "^5.1.0",
|
|
52
|
+
"morgan": "^1.10.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@jest/globals": "^29.7.0",
|
|
56
|
+
"axios-mock-adapter": "^1.22.0",
|
|
57
|
+
"jest": "^29.7.0"
|
|
58
|
+
},
|
|
59
|
+
"jest": {
|
|
60
|
+
"transform": {},
|
|
61
|
+
"testEnvironment": "node",
|
|
62
|
+
"collectCoverageFrom": [
|
|
63
|
+
"index.js",
|
|
64
|
+
"server.js",
|
|
65
|
+
"lib/**/*.js",
|
|
66
|
+
"!node_modules/**"
|
|
67
|
+
],
|
|
68
|
+
"coverageDirectory": "coverage",
|
|
69
|
+
"coverageReporters": [
|
|
70
|
+
"text",
|
|
71
|
+
"lcov",
|
|
72
|
+
"html"
|
|
73
|
+
],
|
|
74
|
+
"testMatch": [
|
|
75
|
+
"**/__tests__/**/*.test.js",
|
|
76
|
+
"**/?(*.)+(spec|test).js"
|
|
77
|
+
]
|
|
45
78
|
},
|
|
46
79
|
"files": [
|
|
47
80
|
"index.js",
|
|
81
|
+
"server.js",
|
|
82
|
+
"lib",
|
|
48
83
|
"README.md",
|
|
49
84
|
"README-ko.md",
|
|
50
85
|
"LICENSE"
|
package/server.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import morgan from 'morgan';
|
|
5
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
|
+
import { createChatGptServer } from './lib/core/server-factory.js';
|
|
7
|
+
import { getConfig } from './lib/core/config-manager.js';
|
|
8
|
+
import { resolveAuthContext } from './lib/auth/strategy-factory.js';
|
|
9
|
+
|
|
10
|
+
const config = getConfig();
|
|
11
|
+
|
|
12
|
+
if (!config.features.sseTransport) {
|
|
13
|
+
console.error('[ainote-mcp] SSE transport is disabled. Set AINOTE_ENABLE_SSE_TRANSPORT=true to enable.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
const sessions = new Map();
|
|
19
|
+
|
|
20
|
+
const corsOptions = {
|
|
21
|
+
origin: config.http.allowedOrigins.length > 0 ? config.http.allowedOrigins : true,
|
|
22
|
+
credentials: true
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
app.use(cors(corsOptions));
|
|
26
|
+
app.use(express.json({ limit: '4mb' }));
|
|
27
|
+
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'));
|
|
28
|
+
|
|
29
|
+
app.get('/health', (_req, res) => {
|
|
30
|
+
res.json({
|
|
31
|
+
status: 'ok',
|
|
32
|
+
transport: 'sse',
|
|
33
|
+
timestamp: Date.now()
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.get(config.http.endpoint, async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { server } = createChatGptServer();
|
|
40
|
+
const transport = new SSEServerTransport(config.http.messageEndpoint, res, {
|
|
41
|
+
enableDnsRebindingProtection: config.http.allowedHosts.length > 0 || config.http.allowedOrigins.length > 0,
|
|
42
|
+
allowedHosts: config.http.allowedHosts.length > 0 ? config.http.allowedHosts : undefined,
|
|
43
|
+
allowedOrigins: config.http.allowedOrigins.length > 0 ? config.http.allowedOrigins : undefined
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
sessions.set(transport.sessionId, { transport, server });
|
|
47
|
+
|
|
48
|
+
transport.onclose = async () => {
|
|
49
|
+
sessions.delete(transport.sessionId);
|
|
50
|
+
try {
|
|
51
|
+
await server.close();
|
|
52
|
+
} catch (closeError) {
|
|
53
|
+
console.error(`[ainote-mcp] Error while closing server: ${closeError.message}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
transport.onerror = (error) => {
|
|
58
|
+
console.error(`[ainote-mcp] SSE transport error: ${error.message}`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await server.connect(transport);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`[ainote-mcp] Failed to establish SSE connection: ${error.message}`);
|
|
64
|
+
if (!res.headersSent) {
|
|
65
|
+
res.status(500).json({ error: 'Failed to establish SSE connection' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.post(config.http.messageEndpoint, async (req, res) => {
|
|
71
|
+
const sessionId = req.query.sessionId;
|
|
72
|
+
|
|
73
|
+
if (!sessionId) {
|
|
74
|
+
return res.status(400).json({ error: 'Missing sessionId query parameter' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const session = sessions.get(sessionId);
|
|
78
|
+
|
|
79
|
+
if (!session) {
|
|
80
|
+
return res.status(404).json({ error: `No transport found for sessionId ${sessionId}` });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const authContext = resolveAuthContext('sse', req.headers);
|
|
85
|
+
req.auth = authContext;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return res.status(401).json({ error: error.message });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await session.transport.handlePostMessage(req, res, req.body);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`[ainote-mcp] Failed to handle POST message: ${error.message}`);
|
|
94
|
+
if (!res.headersSent) {
|
|
95
|
+
res.status(500).json({ error: 'Failed to handle message' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const port = config.http.port;
|
|
101
|
+
app.listen(port, () => {
|
|
102
|
+
console.error(`AI Note MCP HTTP/SSE server listening on port ${port}`);
|
|
103
|
+
});
|