@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,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,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
|
+
}
|