@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.
@@ -0,0 +1,118 @@
1
+ import * as readline from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ import axios from 'axios';
4
+ import { getConfig } from '../core/config-manager.js';
5
+
6
+ /**
7
+ * Interactive CLI signup for AI Note MCP.
8
+ * Usage: ainote-mcp signup
9
+ */
10
+ export async function runSignup() {
11
+ const config = getConfig();
12
+ const rl = readline.createInterface({ input, output });
13
+
14
+ console.log('\n AI Note MCP — Sign Up\n');
15
+
16
+ try {
17
+ const email = await rl.question(' Email: ');
18
+ const password = await rl.question(' Password (min 6 chars): ');
19
+ const name = await rl.question(' Name (optional, press Enter to skip): ');
20
+
21
+ if (!email.trim() || !password.trim()) {
22
+ console.error('\n Error: Email and password are required.\n');
23
+ process.exit(1);
24
+ }
25
+
26
+ if (password.trim().length < 6) {
27
+ console.error('\n Error: Password must be at least 6 characters.\n');
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log('\n Creating account...');
32
+
33
+ const rpcRequest = {
34
+ jsonrpc: '2.0',
35
+ id: 1,
36
+ method: 'tools/call',
37
+ params: {
38
+ name: 'signup_and_get_key',
39
+ arguments: {
40
+ email: email.trim(),
41
+ password: password.trim(),
42
+ ...(name.trim() ? { name: name.trim() } : {})
43
+ }
44
+ }
45
+ };
46
+
47
+ const response = await axios.post(`${config.api.url}/api/mcp`, rpcRequest, {
48
+ headers: { 'Content-Type': 'application/json' },
49
+ timeout: 30000
50
+ });
51
+
52
+ const data = response.data?.data;
53
+
54
+ // Check for JSON-RPC error
55
+ if (data?.error) {
56
+ console.error(`\n Error: ${data.error.message}`);
57
+ if (data.error.data?.suggestion) {
58
+ console.log(` Hint: ${data.error.data.suggestion}`);
59
+ }
60
+ console.log();
61
+ process.exit(1);
62
+ }
63
+
64
+ // Extract MCP key from structured resource data
65
+ const content = data?.result?.content;
66
+ let mcpKey = null;
67
+
68
+ if (Array.isArray(content)) {
69
+ for (const item of content) {
70
+ if (item.type === 'resource' && item.resource?.text) {
71
+ try {
72
+ const resourceData = JSON.parse(item.resource.text);
73
+ mcpKey = resourceData.mcp_key;
74
+ } catch { /* ignore */ }
75
+ }
76
+ }
77
+ }
78
+
79
+ if (mcpKey) {
80
+ console.log('\n Account created successfully!\n');
81
+ console.log(` Your MCP API Key:\n`);
82
+ console.log(` ${mcpKey}\n`);
83
+ console.log(' Add this to your MCP config:\n');
84
+ console.log(' {');
85
+ console.log(' "mcpServers": {');
86
+ console.log(' "ainote": {');
87
+ console.log(' "command": "npx",');
88
+ console.log(' "args": ["-y", "@ainote/mcp"],');
89
+ console.log(' "env": {');
90
+ console.log(` "AINOTE_API_KEY": "${mcpKey}"`);
91
+ console.log(' }');
92
+ console.log(' }');
93
+ console.log(' }');
94
+ console.log(' }\n');
95
+ console.log(' Then restart Claude Desktop.\n');
96
+ } else {
97
+ // Fallback: print text content
98
+ if (Array.isArray(content)) {
99
+ for (const item of content) {
100
+ if (item.type === 'text') {
101
+ console.log(`\n${item.text}\n`);
102
+ }
103
+ }
104
+ } else {
105
+ console.log('\n Account created! Check the response for your API key.\n');
106
+ }
107
+ }
108
+ } catch (error) {
109
+ if (error.response?.data) {
110
+ console.error(`\n Error: ${JSON.stringify(error.response.data)}\n`);
111
+ } else {
112
+ console.error(`\n Error: ${error.message}\n`);
113
+ }
114
+ process.exit(1);
115
+ } finally {
116
+ rl.close();
117
+ }
118
+ }
@@ -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,82 @@
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 { getDevDocTools } from '../tools/dev-doc-tools.js';
9
+ import { getOnboardingTools } from '../tools/onboarding-tools.js';
10
+ import { toErrorContent } from '../tools/internal/formatters.js';
11
+
12
+ function buildServerInfo(profile) {
13
+ return {
14
+ name: `ainote-mcp-${profile}`,
15
+ version: '1.1.0'
16
+ };
17
+ }
18
+
19
+ function registerTools(registry, { includeChatGpt }) {
20
+ // Onboarding tools are always registered (work without API key)
21
+ registry.registerMany(getOnboardingTools());
22
+ registry.registerMany(getSharedTools());
23
+ registry.registerMany(getDevDocTools());
24
+
25
+ if (includeChatGpt) {
26
+ registry.registerMany(getChatGptTools());
27
+ }
28
+ }
29
+
30
+ function createServer({ profile, defaultAuthContext } = {}) {
31
+ const config = getConfig();
32
+ const includeChatGpt = profile === 'chatgpt' && config.features.chatgptSupport;
33
+ const registry = new ToolRegistry();
34
+
35
+ registerTools(registry, { includeChatGpt });
36
+
37
+ const server = new Server(buildServerInfo(profile), {
38
+ capabilities: {
39
+ tools: {}
40
+ }
41
+ });
42
+
43
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
44
+ tools: registry.listDefinitions()
45
+ }));
46
+
47
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
48
+ try {
49
+ const { name, arguments: args } = request.params;
50
+ console.error(`[MCP] Calling tool: ${name}`, JSON.stringify(args));
51
+
52
+ const authContext = extra?.authInfo ?? defaultAuthContext;
53
+ const apiClient = createApiClient(authContext);
54
+
55
+ const result = await registry.execute(name, args, {
56
+ apiClient,
57
+ request,
58
+ extra
59
+ });
60
+
61
+ console.error(`[MCP] Tool ${name} completed successfully`);
62
+ return result;
63
+ } catch (error) {
64
+ console.error(`[MCP] Tool error:`, error);
65
+ return toErrorContent(error);
66
+ }
67
+ });
68
+
69
+ return { server, registry };
70
+ }
71
+
72
+ export function createClaudeServer(options = {}) {
73
+ return createServer({ profile: 'claude', ...options });
74
+ }
75
+
76
+ export function createChatGptServer(options = {}) {
77
+ return createServer({ profile: 'chatgpt', ...options });
78
+ }
79
+
80
+ export function createServerForProfile(profile, options = {}) {
81
+ return createServer({ profile, ...options });
82
+ }
@@ -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,166 @@
1
+ import { toSuccessContent } from './internal/formatters.js';
2
+
3
+ function listDevDocsDefinition() {
4
+ return {
5
+ name: 'list_dev_docs',
6
+ description: `List dev documents under dev/ category hierarchy. Manage AI coding tool configs (CLAUDE.md, .cursorrules, etc.) and dev docs centrally.
7
+
8
+ Categories: claude, cursor, windsurf, copilot, docs, or custom subcategories.
9
+ Supports markdown, json, yaml content types.`,
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ category: {
14
+ type: 'string',
15
+ description: 'Subcategory filter (claude, cursor, windsurf, copilot, docs, etc.)'
16
+ },
17
+ search: {
18
+ type: 'string',
19
+ description: 'Search keyword in document title'
20
+ },
21
+ content_type: {
22
+ type: 'string',
23
+ enum: ['markdown', 'json', 'yaml', 'text'],
24
+ description: 'Filter by content type'
25
+ }
26
+ }
27
+ }
28
+ };
29
+ }
30
+
31
+ function getDevDocDefinition() {
32
+ return {
33
+ name: 'get_dev_doc',
34
+ description: 'Get a single dev document by title or id. Returns full content.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ title: { type: 'string', description: 'Document title (e.g., project-a-memory.md)' },
39
+ id: { type: 'string', description: 'Document UUID' },
40
+ category: { type: 'string', description: 'Subcategory (claude, cursor, etc.) to disambiguate' },
41
+ include_versions: { type: 'boolean', description: 'Include version history (default: false)' }
42
+ }
43
+ }
44
+ };
45
+ }
46
+
47
+ function createDevDocDefinition() {
48
+ return {
49
+ name: 'create_dev_doc',
50
+ description: `Create a new dev document under dev/ category. Auto-creates subcategory if needed.
51
+
52
+ Use this to store CLAUDE.md content, cursor rules, windsurf rules, API specs, architecture docs, etc.`,
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ title: { type: 'string', description: 'Document title (e.g., project-a-claude.md)' },
57
+ content: { type: 'string', description: 'Document content (markdown, json, yaml)' },
58
+ category: {
59
+ type: 'string',
60
+ description: 'Subcategory (claude, cursor, windsurf, copilot, docs). Default: docs'
61
+ },
62
+ content_type: {
63
+ type: 'string',
64
+ enum: ['markdown', 'json', 'yaml', 'text'],
65
+ description: 'Content type. Auto-detected from title extension if omitted.'
66
+ }
67
+ },
68
+ required: ['title', 'content']
69
+ }
70
+ };
71
+ }
72
+
73
+ function updateDevDocDefinition() {
74
+ return {
75
+ name: 'update_dev_doc',
76
+ description: 'Update a dev document. Supports replace, append, or prepend modes.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ title: { type: 'string', description: 'Document title' },
81
+ id: { type: 'string', description: 'Document UUID' },
82
+ category: { type: 'string', description: 'Subcategory to disambiguate' },
83
+ content: { type: 'string', description: 'New content' },
84
+ mode: {
85
+ type: 'string',
86
+ enum: ['replace', 'append', 'prepend'],
87
+ description: 'Update mode (default: replace)'
88
+ }
89
+ },
90
+ required: ['content']
91
+ }
92
+ };
93
+ }
94
+
95
+ function deleteDevDocDefinition() {
96
+ return {
97
+ name: 'delete_dev_doc',
98
+ description: 'Delete a dev document (soft delete).',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ title: { type: 'string', description: 'Document title' },
103
+ id: { type: 'string', description: 'Document UUID' },
104
+ category: { type: 'string', description: 'Subcategory to disambiguate' }
105
+ }
106
+ }
107
+ };
108
+ }
109
+
110
+ function listDevCategoriesDefinition() {
111
+ return {
112
+ name: 'list_dev_categories',
113
+ description: 'List all subcategories under dev/. Shows document count per category.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {}
117
+ }
118
+ };
119
+ }
120
+
121
+ export function getDevDocTools() {
122
+ return [
123
+ {
124
+ definition: listDevDocsDefinition(),
125
+ handler: async (args, { apiClient }) => {
126
+ const result = await apiClient.callTool('list_dev_docs', args);
127
+ return result;
128
+ }
129
+ },
130
+ {
131
+ definition: getDevDocDefinition(),
132
+ handler: async (args, { apiClient }) => {
133
+ const result = await apiClient.callTool('get_dev_doc', args);
134
+ return result;
135
+ }
136
+ },
137
+ {
138
+ definition: createDevDocDefinition(),
139
+ handler: async (args, { apiClient }) => {
140
+ const result = await apiClient.callTool('create_dev_doc', args);
141
+ return result;
142
+ }
143
+ },
144
+ {
145
+ definition: updateDevDocDefinition(),
146
+ handler: async (args, { apiClient }) => {
147
+ const result = await apiClient.callTool('update_dev_doc', args);
148
+ return result;
149
+ }
150
+ },
151
+ {
152
+ definition: deleteDevDocDefinition(),
153
+ handler: async (args, { apiClient }) => {
154
+ const result = await apiClient.callTool('delete_dev_doc', args);
155
+ return result;
156
+ }
157
+ },
158
+ {
159
+ definition: listDevCategoriesDefinition(),
160
+ handler: async (_args, { apiClient }) => {
161
+ const result = await apiClient.callTool('list_dev_categories', {});
162
+ return result;
163
+ }
164
+ }
165
+ ];
166
+ }
@@ -0,0 +1,5 @@
1
+ export { getSharedTools } from './shared-tools.js';
2
+ export { getChatGptTools } from './chatgpt-tools.js';
3
+ export { getDevDocTools } from './dev-doc-tools.js';
4
+ export { getOnboardingTools } from './onboarding-tools.js';
5
+ export { toSuccessContent, toErrorContent } from './internal/formatters.js';
@@ -0,0 +1,80 @@
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
+ }
38
+
39
+ /**
40
+ * Parse structured data from MCP response
41
+ * Looks for JSON resource in content array and extracts structured data
42
+ */
43
+ export function parseStructuredData(result) {
44
+ if (!result?.content || !Array.isArray(result.content)) {
45
+ return null;
46
+ }
47
+
48
+ // Look for resource content with JSON data
49
+ for (const item of result.content) {
50
+ if (item.type === 'resource' && item.resource?.mimeType === 'application/json') {
51
+ try {
52
+ return JSON.parse(item.resource.text);
53
+ } catch (error) {
54
+ console.error('Failed to parse structured data:', error);
55
+ return null;
56
+ }
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Enhance result with structured data annotation
65
+ * Adds structured data to the response while preserving original text
66
+ */
67
+ export function enhanceWithStructuredData(result) {
68
+ const structuredData = parseStructuredData(result);
69
+
70
+ if (structuredData) {
71
+ // Add metadata about structured data availability
72
+ return {
73
+ ...result,
74
+ _structuredData: structuredData,
75
+ _hasStructuredData: true
76
+ };
77
+ }
78
+
79
+ return result;
80
+ }