@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 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
@@ -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
- 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://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
- 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.2",
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
+ });