@ainote/mcp 1.0.1 → 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-ko.md CHANGED
@@ -103,16 +103,48 @@ Claude Desktop을 AI Note 작업 관리 시스템에 연결하는 MCP(Model Cont
103
103
 
104
104
  ## 설치
105
105
 
106
- ### 설치
106
+ ### 옵션 1: npm에서 설치 (권장)
107
107
 
108
- 소스코드로부터 설치
108
+ ```bash
109
+ npm install -g @ainote/mcp
110
+ ```
111
+
112
+ ### 옵션 2: 소스코드로부터 설치
113
+
114
+ ```bash
115
+ git clone https://github.com/ainote-dev/ainote-mcp.git
116
+ cd ainote-mcp-server
117
+ npm install
118
+ ```
119
+
120
+ ## 업데이트
121
+
122
+ ### npm에서 업데이트 (권장)
123
+
124
+ npm을 통해 설치한 경우 최신 버전으로 업데이트:
125
+
126
+ ```bash
127
+ npm update -g @ainote/mcp
128
+ ```
129
+
130
+ 특정 버전을 설치하려면:
131
+
132
+ ```bash
133
+ npm install -g @ainote/mcp@1.0.2
134
+ ```
135
+
136
+ ### 소스코드에서 업데이트
137
+
138
+ 소스코드로 설치한 경우:
109
139
 
110
140
  ```bash
111
- git clone https://github.com/your-username/ainote-mcp-server.git
112
141
  cd ainote-mcp-server
142
+ git pull origin main
113
143
  npm install
114
144
  ```
115
145
 
146
+ 업데이트 후에는 **Claude Desktop을 재시작**하여 새 버전을 로드하세요.
147
+
116
148
  ## 설정
117
149
 
118
150
  ### 1. 환경 변수 설정
package/README.md CHANGED
@@ -3,15 +3,19 @@
3
3
  [![npm version](https://badge.fury.io/js/%40ainote%2Fmcp-server.svg)](https://badge.fury.io/js/%40ainote%2Fmcp-server)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A Model Context Protocol (MCP) server that connects Claude Desktop to your AI Note task management system. This server allows Claude to directly interact with your AI Note tasks, enabling task creation, updates, and management through natural language conversations.
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 >= 16.0.0
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
@@ -51,6 +81,34 @@ cd ainote-mcp-server
51
81
  npm install
52
82
  ```
53
83
 
84
+ ## Update
85
+
86
+ ### Update from npm (Recommended)
87
+
88
+ To update to the latest version when installed via npm:
89
+
90
+ ```bash
91
+ npm update -g @ainote/mcp
92
+ ```
93
+
94
+ Or to install a specific version:
95
+
96
+ ```bash
97
+ npm install -g @ainote/mcp@1.0.2
98
+ ```
99
+
100
+ ### Update from source
101
+
102
+ If you installed from source:
103
+
104
+ ```bash
105
+ cd ainote-mcp-server
106
+ git pull origin main
107
+ npm install
108
+ ```
109
+
110
+ After updating, **restart Claude Desktop** to load the new version.
111
+
54
112
  ## Configuration
55
113
 
56
114
  To connect the MCP server with Claude Desktop, follow these steps.
@@ -111,6 +169,81 @@ You: "Mark task ID 123 as completed"
111
169
  Claude: I'll mark that task as completed...
112
170
  ```
113
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
+
114
247
  ## Available Tools
115
248
 
116
249
  ### list_tasks
package/index.js CHANGED
@@ -1,229 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
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://ainote-5muq.onrender.com';
9
-
10
- // Create axios instance with default config
11
- const api = axios.create({
12
- baseURL: API_URL,
13
- headers: {
14
- 'Authorization': 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
- try {
141
- switch (name) {
142
- case 'list_tasks': {
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
- case 'update_task': {
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
- case 'delete_task': {
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
- case 'list_categories': {
198
- const response = await api.get('/api/mcp/categories');
199
- return {
200
- content: [
201
- {
202
- type: 'text',
203
- text: JSON.stringify(response.data, null, 2)
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
- default:
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,2 @@
1
+ // Deprecated: use server-factory instead.
2
+ export { createClaudeServer, createChatGptServer, createServerForProfile } from './server-factory.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,3 @@
1
+ export { getSharedTools } from './shared-tools.js';
2
+ export { getChatGptTools } from './chatgpt-tools.js';
3
+ export { toSuccessContent, toErrorContent } from './internal/formatters.js';
@@ -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.1",
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
- "test": "echo \"Error: no test specified\" && exit 1"
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": ">=16.0.0"
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
+ });