@ainote/mcp 1.0.2 → 1.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.
- package/README.md +313 -8
- package/index.js +27 -225
- package/lib/auth/mcp-key.js +14 -0
- package/lib/auth/oauth.js +18 -0
- package/lib/auth/strategy-factory.js +31 -0
- package/lib/cli/signup.js +118 -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 +82 -0
- package/lib/core/tool-registry.js +31 -0
- package/lib/tools/chatgpt-tools.js +93 -0
- package/lib/tools/dev-doc-tools.js +166 -0
- package/lib/tools/index.js +5 -0
- package/lib/tools/internal/formatters.js +80 -0
- package/lib/tools/onboarding-tools.js +124 -0
- package/lib/tools/shared-tools.js +246 -0
- package/lib/utils/api-client.js +79 -0
- package/package.json +48 -6
- package/server.js +99 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { getConfig } from '../core/config-manager.js';
|
|
3
|
+
import { toErrorContent } from './internal/formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Onboarding tools for signup/login without API key.
|
|
7
|
+
* These tools call the Rails API directly (no auth headers).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function signupDefinition() {
|
|
11
|
+
return {
|
|
12
|
+
name: 'signup_and_get_key',
|
|
13
|
+
description: `Create a new AI Note account and get an MCP API key. No authentication required.
|
|
14
|
+
Use this if you don't have an account yet. After getting the key, add it to your MCP config and restart.`,
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
email: { type: 'string', description: 'Email address for the new account' },
|
|
19
|
+
password: { type: 'string', description: 'Password (minimum 6 characters)' },
|
|
20
|
+
name: { type: 'string', description: 'Display name (optional)' }
|
|
21
|
+
},
|
|
22
|
+
required: ['email', 'password']
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loginDefinition() {
|
|
28
|
+
return {
|
|
29
|
+
name: 'login_and_get_key',
|
|
30
|
+
description: `Log in to an existing AI Note account and get an MCP API key. No authentication required.
|
|
31
|
+
Use this if you already have an account but need your MCP key.`,
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
email: { type: 'string', description: 'Your account email address' },
|
|
36
|
+
password: { type: 'string', description: 'Your account password' }
|
|
37
|
+
},
|
|
38
|
+
required: ['email', 'password']
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setupGuideDefinition() {
|
|
44
|
+
return {
|
|
45
|
+
name: 'get_setup_guide',
|
|
46
|
+
description: 'Get instructions for setting up AI Note MCP in Claude Desktop, Cursor, or other MCP clients. Includes CLI signup method (npx @ainote/mcp signup). No authentication required.',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Call Rails MCP API directly without auth headers (for onboarding tools).
|
|
56
|
+
*/
|
|
57
|
+
async function callOnboardingTool(toolName, args = {}) {
|
|
58
|
+
const config = getConfig();
|
|
59
|
+
const rpcRequest = {
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
id: Date.now(),
|
|
62
|
+
method: 'tools/call',
|
|
63
|
+
params: { name: toolName, arguments: args }
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const response = await axios.post(`${config.api.url}/api/mcp`, rpcRequest, {
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
timeout: 30000
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (response.data?.data?.error) {
|
|
72
|
+
const err = response.data.data.error;
|
|
73
|
+
throw new Error(err.message || 'Onboarding tool call failed');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Rails wraps in { success: true, data: { jsonrpc, id, result } }
|
|
77
|
+
const rpcResult = response.data?.data?.result;
|
|
78
|
+
if (rpcResult) {
|
|
79
|
+
return rpcResult;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fallback: return the raw response content
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: JSON.stringify(response.data, null, 2)
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getOnboardingTools() {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
definition: signupDefinition(),
|
|
95
|
+
handler: async (args) => {
|
|
96
|
+
try {
|
|
97
|
+
return await callOnboardingTool('signup_and_get_key', args);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return toErrorContent(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
definition: loginDefinition(),
|
|
105
|
+
handler: async (args) => {
|
|
106
|
+
try {
|
|
107
|
+
return await callOnboardingTool('login_and_get_key', args);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return toErrorContent(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
definition: setupGuideDefinition(),
|
|
115
|
+
handler: async () => {
|
|
116
|
+
try {
|
|
117
|
+
return await callOnboardingTool('get_setup_guide');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return toErrorContent(error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { toSuccessContent, enhanceWithStructuredData } from './internal/formatters.js';
|
|
2
|
+
|
|
3
|
+
function listTasksDefinition() {
|
|
4
|
+
return {
|
|
5
|
+
name: 'list_tasks',
|
|
6
|
+
description: `List tasks from AI Note with natural language support and advanced filtering.
|
|
7
|
+
|
|
8
|
+
NATURAL LANGUAGE EXAMPLES:
|
|
9
|
+
• Location: "여의도에서", "서울에 있는", "강남 관련"
|
|
10
|
+
• Time: "오늘", "이번 주", "다음 주", "지난달", "1월에"
|
|
11
|
+
• Importance: "중요한", "우선순위 높은", "급한"
|
|
12
|
+
• Status: "완료한", "미완료", "안 끝난"
|
|
13
|
+
• Special: "마감일 지난", "늦어진", "오늘 마감", "알림 설정된"
|
|
14
|
+
• Sort: "마감일 순으로", "최신순으로", "오래된 순으로"
|
|
15
|
+
|
|
16
|
+
QUERY EXAMPLES:
|
|
17
|
+
1. "여의도에서 이번 주 마감인 중요한 미완료 할일"
|
|
18
|
+
→ {location: "여의도", due_date_start: "2025-01-27", due_date_end: "2025-02-02",
|
|
19
|
+
is_important: true, status: "pending"}
|
|
20
|
+
|
|
21
|
+
2. "지난달 완료한 업무 카테고리 할일들을 완료일 순으로"
|
|
22
|
+
→ {category_id: "...", status: "completed",
|
|
23
|
+
completed_date_start: "2024-12-01", completed_date_end: "2024-12-31",
|
|
24
|
+
sort_by: "completed_at", sort_order: "asc"}
|
|
25
|
+
|
|
26
|
+
3. "마감일 지났는데 아직 안 끝난 할일들 마감일 빠른 순으로"
|
|
27
|
+
→ {overdue: true, sort_by: "due_date", sort_order: "asc"}
|
|
28
|
+
|
|
29
|
+
TIME CALCULATIONS (today = 2025-01-27):
|
|
30
|
+
• "오늘" → due_today: true
|
|
31
|
+
• "이번 주" → due_date_start: "2025-01-27", due_date_end: "2025-02-02"
|
|
32
|
+
• "다음 주" → due_date_start: "2025-02-03", due_date_end: "2025-02-09"
|
|
33
|
+
• "이번 달" → due_date_start: "2025-01-01", due_date_end: "2025-01-31"
|
|
34
|
+
• "지난 주" → completed_date_start: "2025-01-20", completed_date_end: "2025-01-26"
|
|
35
|
+
• "지난 달" → completed_date_start: "2024-12-01", completed_date_end: "2024-12-31"
|
|
36
|
+
|
|
37
|
+
Returns structured data with all task fields including location, dates, and categories.`,
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
status: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
enum: ['pending', 'completed'],
|
|
44
|
+
description: 'Filter by task status'
|
|
45
|
+
},
|
|
46
|
+
is_important: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
description: 'Filter by important tasks only'
|
|
49
|
+
},
|
|
50
|
+
limit: {
|
|
51
|
+
type: 'number',
|
|
52
|
+
description: 'Maximum number of tasks to return (default: 25, max: 500)'
|
|
53
|
+
},
|
|
54
|
+
search: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Search keyword in task content'
|
|
57
|
+
},
|
|
58
|
+
location: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'Filter by location (partial match, e.g., "여의도", "서울")'
|
|
61
|
+
},
|
|
62
|
+
category_id: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Filter by category UUID'
|
|
65
|
+
},
|
|
66
|
+
due_date_start: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Filter tasks with due_date >= this date (ISO 8601 format)'
|
|
69
|
+
},
|
|
70
|
+
due_date_end: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Filter tasks with due_date <= this date (ISO 8601 format)'
|
|
73
|
+
},
|
|
74
|
+
completed_date_start: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Filter tasks completed >= this date (ISO 8601 format)'
|
|
77
|
+
},
|
|
78
|
+
completed_date_end: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'Filter tasks completed <= this date (ISO 8601 format)'
|
|
81
|
+
},
|
|
82
|
+
created_date_start: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Filter tasks created >= this date (ISO 8601 format)'
|
|
85
|
+
},
|
|
86
|
+
created_date_end: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Filter tasks created <= this date (ISO 8601 format)'
|
|
89
|
+
},
|
|
90
|
+
overdue: {
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
description: 'Filter overdue incomplete tasks (due_date < today)'
|
|
93
|
+
},
|
|
94
|
+
due_today: {
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
description: 'Filter tasks due today'
|
|
97
|
+
},
|
|
98
|
+
has_notification: {
|
|
99
|
+
type: 'boolean',
|
|
100
|
+
description: 'Filter by notification enabled status'
|
|
101
|
+
},
|
|
102
|
+
sort_by: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
enum: ['due_date', 'created_at', 'completed_at', 'updated_at', 'is_important'],
|
|
105
|
+
description: 'Sort field (default: created_at)'
|
|
106
|
+
},
|
|
107
|
+
sort_order: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
enum: ['asc', 'desc'],
|
|
110
|
+
description: 'Sort order (default: desc)'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createTaskDefinition() {
|
|
118
|
+
return {
|
|
119
|
+
name: 'create_task',
|
|
120
|
+
description: 'Create a new task in AI Note with optional notification',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
content: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Task content'
|
|
127
|
+
},
|
|
128
|
+
is_important: {
|
|
129
|
+
type: 'boolean',
|
|
130
|
+
description: 'Mark task as important'
|
|
131
|
+
},
|
|
132
|
+
due_date: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'Due date in ISO format'
|
|
135
|
+
},
|
|
136
|
+
category_id: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Category ID'
|
|
139
|
+
},
|
|
140
|
+
notification_minutes_before: {
|
|
141
|
+
type: 'number',
|
|
142
|
+
description: 'Set notification X minutes before due_date (e.g., 5 for 5 minutes before, 60 for 1 hour before). Requires due_date to be set.'
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
required: ['content']
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function updateTaskDefinition() {
|
|
151
|
+
return {
|
|
152
|
+
name: 'update_task',
|
|
153
|
+
description: 'Update an existing task',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
id: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description: 'Task ID'
|
|
160
|
+
},
|
|
161
|
+
content: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'New task content'
|
|
164
|
+
},
|
|
165
|
+
is_important: {
|
|
166
|
+
type: 'boolean',
|
|
167
|
+
description: 'Update important status'
|
|
168
|
+
},
|
|
169
|
+
completed_at: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'Mark as completed (ISO format) or null to uncomplete'
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
required: ['id']
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function deleteTaskDefinition() {
|
|
180
|
+
return {
|
|
181
|
+
name: 'delete_task',
|
|
182
|
+
description: 'Delete a task (soft delete)',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
id: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'Task ID to delete'
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
required: ['id']
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function listCategoriesDefinition() {
|
|
197
|
+
return {
|
|
198
|
+
name: 'list_categories',
|
|
199
|
+
description: 'List all categories',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getSharedTools() {
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
definition: listTasksDefinition(),
|
|
211
|
+
handler: async (args, { apiClient }) => {
|
|
212
|
+
const result = await apiClient.callTool('list_tasks', args);
|
|
213
|
+
// Enhance result with parsed structured data
|
|
214
|
+
return enhanceWithStructuredData(result);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
definition: createTaskDefinition(),
|
|
219
|
+
handler: async (args, { apiClient }) => {
|
|
220
|
+
const result = await apiClient.callTool('create_task', args);
|
|
221
|
+
return result; // Return full result with { content: [...] }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
definition: updateTaskDefinition(),
|
|
226
|
+
handler: async (args, { apiClient }) => {
|
|
227
|
+
const result = await apiClient.callTool('update_task', args);
|
|
228
|
+
return result; // Return full result with { content: [...] }
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
definition: deleteTaskDefinition(),
|
|
233
|
+
handler: async (args, { apiClient }) => {
|
|
234
|
+
const result = await apiClient.callTool('delete_task', args);
|
|
235
|
+
return result; // Return full result with { content: [...] }
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
definition: listCategoriesDefinition(),
|
|
240
|
+
handler: async (_args, { apiClient }) => {
|
|
241
|
+
const result = await apiClient.callTool('list_categories', {});
|
|
242
|
+
return result; // Return full result with { content: [...] }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 === 'anonymous') {
|
|
12
|
+
// No auth headers for anonymous mode (onboarding tools)
|
|
13
|
+
return headers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (contextType === 'oauth') {
|
|
17
|
+
const bearerToken = authContext?.token;
|
|
18
|
+
if (!bearerToken) {
|
|
19
|
+
throw new Error('OAuth authentication requires a bearer token');
|
|
20
|
+
}
|
|
21
|
+
headers.Authorization = `Bearer ${bearerToken}`;
|
|
22
|
+
} else {
|
|
23
|
+
const apiKey = authContext?.apiKey ?? config.api.key;
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
throw new Error('AINOTE_API_KEY environment variable is required for MCP authentication');
|
|
26
|
+
}
|
|
27
|
+
headers.Authorization = `McpKey ${apiKey}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let requestIdCounter = 1;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* JSON-RPC 2.0 요청 생성 헬퍼
|
|
37
|
+
*/
|
|
38
|
+
function createJsonRpcRequest(method, params = {}) {
|
|
39
|
+
return {
|
|
40
|
+
jsonrpc: '2.0',
|
|
41
|
+
id: requestIdCounter++,
|
|
42
|
+
method,
|
|
43
|
+
params
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* JSON-RPC 2.0 tools/call 요청 생성
|
|
49
|
+
*/
|
|
50
|
+
function createToolCallRequest(toolName, arguments_) {
|
|
51
|
+
return createJsonRpcRequest('tools/call', {
|
|
52
|
+
name: toolName,
|
|
53
|
+
arguments: arguments_
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createApiClient(authContext) {
|
|
58
|
+
const config = getConfig();
|
|
59
|
+
const headers = buildHeaders(authContext, config);
|
|
60
|
+
|
|
61
|
+
const axiosInstance = axios.create({
|
|
62
|
+
baseURL: config.api.url,
|
|
63
|
+
headers
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// JSON-RPC helper 메서드 추가
|
|
67
|
+
axiosInstance.callTool = async (toolName, arguments_) => {
|
|
68
|
+
const rpcRequest = createToolCallRequest(toolName, arguments_);
|
|
69
|
+
const response = await axiosInstance.post('/api/mcp', rpcRequest);
|
|
70
|
+
|
|
71
|
+
if (response.data.error) {
|
|
72
|
+
throw new Error(`JSON-RPC Error [${response.data.error.code}]: ${response.data.error.message}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return response.data.result;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return axiosInstance;
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainote/mcp",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "MCP server for AI Note -
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP server for AI Note - Tasks + Dev Docs (CLAUDE.md, Cursor rules, Windsurf rules) management via MCP",
|
|
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",
|
|
16
21
|
"modelcontextprotocol",
|
|
17
22
|
"model-context-protocol",
|
|
18
23
|
"claude",
|
|
24
|
+
"claude-code",
|
|
19
25
|
"claude-desktop",
|
|
26
|
+
"cursor",
|
|
27
|
+
"windsurf",
|
|
28
|
+
"copilot",
|
|
20
29
|
"ainote",
|
|
21
30
|
"ai-note",
|
|
31
|
+
"dev-docs",
|
|
32
|
+
"claude-md",
|
|
33
|
+
"memory",
|
|
22
34
|
"tasks",
|
|
23
35
|
"task-management",
|
|
24
36
|
"todo",
|
|
@@ -37,14 +49,44 @@
|
|
|
37
49
|
},
|
|
38
50
|
"homepage": "https://github.com/ainote-dev/ainote-mcp#readme",
|
|
39
51
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
52
|
+
"node": ">=18.0.0"
|
|
41
53
|
},
|
|
42
54
|
"dependencies": {
|
|
43
55
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
|
-
"axios": "^1.6.0"
|
|
56
|
+
"axios": "^1.6.0",
|
|
57
|
+
"cors": "^2.8.5",
|
|
58
|
+
"express": "^5.1.0",
|
|
59
|
+
"morgan": "^1.10.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@jest/globals": "^29.7.0",
|
|
63
|
+
"axios-mock-adapter": "^1.22.0",
|
|
64
|
+
"jest": "^29.7.0"
|
|
65
|
+
},
|
|
66
|
+
"jest": {
|
|
67
|
+
"transform": {},
|
|
68
|
+
"testEnvironment": "node",
|
|
69
|
+
"collectCoverageFrom": [
|
|
70
|
+
"index.js",
|
|
71
|
+
"server.js",
|
|
72
|
+
"lib/**/*.js",
|
|
73
|
+
"!node_modules/**"
|
|
74
|
+
],
|
|
75
|
+
"coverageDirectory": "coverage",
|
|
76
|
+
"coverageReporters": [
|
|
77
|
+
"text",
|
|
78
|
+
"lcov",
|
|
79
|
+
"html"
|
|
80
|
+
],
|
|
81
|
+
"testMatch": [
|
|
82
|
+
"**/__tests__/**/*.test.js",
|
|
83
|
+
"**/?(*.)+(spec|test).js"
|
|
84
|
+
]
|
|
45
85
|
},
|
|
46
86
|
"files": [
|
|
47
87
|
"index.js",
|
|
88
|
+
"server.js",
|
|
89
|
+
"lib",
|
|
48
90
|
"README.md",
|
|
49
91
|
"README-ko.md",
|
|
50
92
|
"LICENSE"
|
package/server.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
const authContext = resolveAuthContext('sse', req.headers);
|
|
84
|
+
req.auth = authContext;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await session.transport.handlePostMessage(req, res, req.body);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`[ainote-mcp] Failed to handle POST message: ${error.message}`);
|
|
90
|
+
if (!res.headersSent) {
|
|
91
|
+
res.status(500).json({ error: 'Failed to handle message' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const port = config.http.port;
|
|
97
|
+
app.listen(port, () => {
|
|
98
|
+
console.error(`AI Note MCP HTTP/SSE server listening on port ${port}`);
|
|
99
|
+
});
|