kmemo-mcp 1.0.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/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/lib/api-client.d.ts +9 -0
- package/dist/lib/api-client.js +64 -0
- package/dist/tools/arrange.d.ts +2 -0
- package/dist/tools/arrange.js +84 -0
- package/dist/tools/files.d.ts +2 -0
- package/dist/tools/files.js +104 -0
- package/dist/tools/notes.d.ts +2 -0
- package/dist/tools/notes.js +144 -0
- package/dist/tools/pages.d.ts +2 -0
- package/dist/tools/pages.js +21 -0
- package/package.json +36 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { registerNoteTools } from './tools/notes.js';
|
|
5
|
+
import { registerArrangeTools } from './tools/arrange.js';
|
|
6
|
+
import { registerFileTools } from './tools/files.js';
|
|
7
|
+
import { registerPageTools } from './tools/pages.js';
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'sticky-notes',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
});
|
|
12
|
+
registerNoteTools(server);
|
|
13
|
+
registerArrangeTools(server);
|
|
14
|
+
registerFileTools(server);
|
|
15
|
+
registerPageTools(server);
|
|
16
|
+
async function main() {
|
|
17
|
+
const transport = new StdioServerTransport();
|
|
18
|
+
await server.connect(transport);
|
|
19
|
+
}
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
console.error('Server failed to start:', err);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API client for MCP server.
|
|
3
|
+
* Calls the deployed website API with MEMO_TOKEN authentication.
|
|
4
|
+
* No Supabase SDK, no .env file, no service keys.
|
|
5
|
+
*/
|
|
6
|
+
export declare function apiGet(path: string, params?: Record<string, string>): Promise<unknown>;
|
|
7
|
+
export declare function apiPost(path: string, body?: unknown): Promise<unknown>;
|
|
8
|
+
export declare function apiPut(path: string, body?: unknown): Promise<unknown>;
|
|
9
|
+
export declare function apiDelete(path: string, body?: unknown): Promise<unknown>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API client for MCP server.
|
|
3
|
+
* Calls the deployed website API with MEMO_TOKEN authentication.
|
|
4
|
+
* No Supabase SDK, no .env file, no service keys.
|
|
5
|
+
*/
|
|
6
|
+
const API_URL = process.env.API_URL || 'https://kmemo.vercel.app';
|
|
7
|
+
const MEMO_TOKEN = process.env.MEMO_TOKEN;
|
|
8
|
+
if (!MEMO_TOKEN) {
|
|
9
|
+
console.error('Missing MEMO_TOKEN. Generate one from your memo app settings.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const headers = {
|
|
13
|
+
'Authorization': `Bearer ${MEMO_TOKEN}`,
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
};
|
|
16
|
+
let tokenWarningShown = false;
|
|
17
|
+
function checkTokenExpiryWarning(res) {
|
|
18
|
+
if (res.headers.get('X-Token-Expires-Soon') === 'true' && !tokenWarningShown) {
|
|
19
|
+
const daysRemaining = res.headers.get('X-Token-Days-Remaining') || '?';
|
|
20
|
+
console.error(`\n[WARNING] Your access token expires in ${daysRemaining} day(s).` +
|
|
21
|
+
`\nGenerate a new token from your memo app settings before it expires.\n`);
|
|
22
|
+
tokenWarningShown = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function handleResponse(res) {
|
|
26
|
+
checkTokenExpiryWarning(res);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const body = await res.text();
|
|
29
|
+
throw new Error(`API ${res.status}: ${body}`);
|
|
30
|
+
}
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
export async function apiGet(path, params) {
|
|
34
|
+
const url = new URL(path, API_URL);
|
|
35
|
+
if (params) {
|
|
36
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
37
|
+
}
|
|
38
|
+
const res = await fetch(url.toString(), { headers });
|
|
39
|
+
return handleResponse(res);
|
|
40
|
+
}
|
|
41
|
+
export async function apiPost(path, body) {
|
|
42
|
+
const res = await fetch(new URL(path, API_URL).toString(), {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers,
|
|
45
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
46
|
+
});
|
|
47
|
+
return handleResponse(res);
|
|
48
|
+
}
|
|
49
|
+
export async function apiPut(path, body) {
|
|
50
|
+
const res = await fetch(new URL(path, API_URL).toString(), {
|
|
51
|
+
method: 'PUT',
|
|
52
|
+
headers,
|
|
53
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
54
|
+
});
|
|
55
|
+
return handleResponse(res);
|
|
56
|
+
}
|
|
57
|
+
export async function apiDelete(path, body) {
|
|
58
|
+
const res = await fetch(new URL(path, API_URL).toString(), {
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
headers,
|
|
61
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
62
|
+
});
|
|
63
|
+
return handleResponse(res);
|
|
64
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { apiGet, apiPost, apiPut } from '../lib/api-client.js';
|
|
3
|
+
export function registerArrangeTools(server) {
|
|
4
|
+
server.tool('arrange_notes', 'Arrange notes on a page into a layout pattern.', {
|
|
5
|
+
page: z.number().describe('Page number to arrange'),
|
|
6
|
+
layout: z.enum(['grid', 'row', 'column', 'cascade', 'stack']).describe('Layout type'),
|
|
7
|
+
columns: z.number().optional().describe('Number of columns for grid layout (default: 3)'),
|
|
8
|
+
gap: z.number().optional().describe('Gap between notes in pixels (default: 30)'),
|
|
9
|
+
startX: z.number().optional().describe('Starting X position (default: 50)'),
|
|
10
|
+
startY: z.number().optional().describe('Starting Y position (default: 50)'),
|
|
11
|
+
}, async ({ page, layout, columns, gap, startX, startY }) => {
|
|
12
|
+
try {
|
|
13
|
+
const result = await apiPost('/api/mcp/notes/arrange', {
|
|
14
|
+
page,
|
|
15
|
+
layout,
|
|
16
|
+
columns: columns ?? 3,
|
|
17
|
+
gap: gap ?? 30,
|
|
18
|
+
startX: startX ?? 50,
|
|
19
|
+
startY: startY ?? 50,
|
|
20
|
+
});
|
|
21
|
+
const layoutDesc = layout === 'grid' ? `${columns ?? 3}-column grid` : layout;
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: `Arranged ${result.arranged} notes on page ${page} in ${layoutDesc} layout.`,
|
|
26
|
+
}],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
server.tool('move_note', 'Move a note to a specific position and/or resize it.', {
|
|
34
|
+
id: z.string().describe('Note UUID'),
|
|
35
|
+
x: z.number().optional().describe('New X position'),
|
|
36
|
+
y: z.number().optional().describe('New Y position'),
|
|
37
|
+
w: z.number().optional().describe('New width'),
|
|
38
|
+
h: z.number().optional().describe('New height'),
|
|
39
|
+
}, async ({ id, x, y, w, h }) => {
|
|
40
|
+
try {
|
|
41
|
+
// First get current note to merge position/size
|
|
42
|
+
const current = await apiGet(`/api/mcp/notes/${id}`);
|
|
43
|
+
const body = {};
|
|
44
|
+
if (x !== undefined || y !== undefined) {
|
|
45
|
+
body.position = {
|
|
46
|
+
x: x ?? current.note.position.x,
|
|
47
|
+
y: y ?? current.note.position.y,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (w !== undefined || h !== undefined) {
|
|
51
|
+
body.size = {
|
|
52
|
+
w: w ?? current.note.size.w,
|
|
53
|
+
h: h ?? current.note.size.h,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
await apiPut(`/api/mcp/notes/${id}`, body);
|
|
57
|
+
return { content: [{ type: 'text', text: `Note ${id} moved/resized successfully.` }] };
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
server.tool('move_notes_to_page', 'Move one or more notes to a different page.', {
|
|
64
|
+
ids: z.array(z.string()).describe('Array of note UUIDs to move'),
|
|
65
|
+
targetPage: z.number().describe('Target page number'),
|
|
66
|
+
}, async ({ ids, targetPage }) => {
|
|
67
|
+
try {
|
|
68
|
+
await apiPost('/api/mcp/notes/arrange', {
|
|
69
|
+
action: 'move_page',
|
|
70
|
+
ids,
|
|
71
|
+
target_page: targetPage,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: `Moved ${ids.length} note(s) to page ${targetPage}.`,
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { apiPost, apiGet, apiPut } from '../lib/api-client.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
export function registerFileTools(server) {
|
|
6
|
+
server.tool('upload_file', 'Upload a local file to storage. Returns the public URL.', {
|
|
7
|
+
filePath: z.string().describe('Absolute path to the local file'),
|
|
8
|
+
}, async ({ filePath }) => {
|
|
9
|
+
try {
|
|
10
|
+
const buffer = await fs.readFile(filePath);
|
|
11
|
+
const base64 = buffer.toString('base64');
|
|
12
|
+
const fileName = path.basename(filePath);
|
|
13
|
+
const result = await apiPost('/api/mcp/upload', {
|
|
14
|
+
file_base64: base64,
|
|
15
|
+
file_name: fileName,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: `File uploaded successfully.\nOriginal: ${fileName}\nURL: ${result.url}`,
|
|
21
|
+
}],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26
|
+
return { content: [{ type: 'text', text: `Failed to upload file: ${msg}` }] };
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
server.tool('attach_file_to_note', 'Attach an uploaded file to a note.', {
|
|
30
|
+
noteId: z.string().describe('Note UUID to attach file to'),
|
|
31
|
+
fileUrl: z.string().optional().describe('Public URL of already uploaded file'),
|
|
32
|
+
filePath: z.string().optional().describe('Local file path to upload and attach'),
|
|
33
|
+
fileName: z.string().optional().describe('Display name for the attachment'),
|
|
34
|
+
}, async ({ noteId, fileUrl, filePath, fileName }) => {
|
|
35
|
+
try {
|
|
36
|
+
if (filePath) {
|
|
37
|
+
// Upload and attach in one call
|
|
38
|
+
const buffer = await fs.readFile(filePath);
|
|
39
|
+
const base64 = buffer.toString('base64');
|
|
40
|
+
const name = fileName || path.basename(filePath);
|
|
41
|
+
const result = await apiPost('/api/mcp/upload', {
|
|
42
|
+
file_base64: base64,
|
|
43
|
+
file_name: path.basename(filePath),
|
|
44
|
+
note_id: noteId,
|
|
45
|
+
display_name: name,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `File attached to note ${noteId}.\nName: ${result.attachment.name}\nType: ${result.attachment.type}\nURL: ${result.attachment.url}`,
|
|
51
|
+
}],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!fileUrl) {
|
|
55
|
+
return { content: [{ type: 'text', text: 'Provide either fileUrl or filePath.' }] };
|
|
56
|
+
}
|
|
57
|
+
// Attach existing URL - update note attachments via PUT
|
|
58
|
+
const noteResult = await apiGet(`/api/mcp/notes/${noteId}`);
|
|
59
|
+
const currentAttachments = noteResult.note.attachments || [];
|
|
60
|
+
const ext = path.extname(fileUrl).toLowerCase();
|
|
61
|
+
const isImage = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'].includes(ext);
|
|
62
|
+
const newAttachment = {
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
name: fileName || path.basename(fileUrl),
|
|
65
|
+
url: fileUrl,
|
|
66
|
+
type: isImage ? 'image' : 'document',
|
|
67
|
+
};
|
|
68
|
+
await apiPut(`/api/mcp/notes/${noteId}`, {
|
|
69
|
+
attachments: [...currentAttachments, newAttachment],
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
content: [{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: `File attached to note ${noteId}.\nName: ${newAttachment.name}\nType: ${newAttachment.type}\nURL: ${newAttachment.url}`,
|
|
75
|
+
}],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
return { content: [{ type: 'text', text: `Failed to attach file: ${msg}` }] };
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.tool('list_attachments', 'List all attachments of a specific note.', {
|
|
84
|
+
noteId: z.string().describe('Note UUID'),
|
|
85
|
+
}, async ({ noteId }) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await apiGet(`/api/mcp/notes/${noteId}`);
|
|
88
|
+
const { title, attachments } = result.note;
|
|
89
|
+
if (!attachments?.length) {
|
|
90
|
+
return { content: [{ type: 'text', text: `Note "${title}" has no attachments.` }] };
|
|
91
|
+
}
|
|
92
|
+
const lines = attachments.map((a, i) => `${i + 1}. ${a.name} (${a.type}) - ${a.url}`);
|
|
93
|
+
return {
|
|
94
|
+
content: [{
|
|
95
|
+
type: 'text',
|
|
96
|
+
text: `Attachments for "${title}" (${attachments.length}):\n\n${lines.join('\n')}`,
|
|
97
|
+
}],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { apiGet, apiPost, apiPut, apiDelete } from '../lib/api-client.js';
|
|
3
|
+
function formatNote(note) {
|
|
4
|
+
const parts = [
|
|
5
|
+
`[${note.id}] ${note.title || '(Untitled)'}`,
|
|
6
|
+
`Page: ${note.page_id} | Color: ${note.color}`,
|
|
7
|
+
`Position: (${note.position.x}, ${note.position.y}) | Size: ${note.size.w}x${note.size.h}`,
|
|
8
|
+
`Created: ${note.created_at} | Updated: ${note.updated_at}`,
|
|
9
|
+
];
|
|
10
|
+
if (note.content) {
|
|
11
|
+
parts.push(`\n--- Content ---\n${note.content}`);
|
|
12
|
+
}
|
|
13
|
+
if (note.attachments?.length) {
|
|
14
|
+
parts.push(`\n--- Attachments (${note.attachments.length}) ---`);
|
|
15
|
+
for (const a of note.attachments) {
|
|
16
|
+
parts.push(` - ${a.name} (${a.type}): ${a.url}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parts.join('\n');
|
|
20
|
+
}
|
|
21
|
+
function formatSummary(note) {
|
|
22
|
+
const preview = note.content
|
|
23
|
+
? note.content.substring(0, 80).replace(/\n/g, ' ')
|
|
24
|
+
: '';
|
|
25
|
+
const attachCount = note.attachments?.length || 0;
|
|
26
|
+
const attachInfo = attachCount > 0 ? ` [${attachCount} files]` : '';
|
|
27
|
+
return `[${note.id}] ${note.title || '(Untitled)'}${attachInfo} - ${preview}`;
|
|
28
|
+
}
|
|
29
|
+
export function registerNoteTools(server) {
|
|
30
|
+
server.tool('list_notes', 'List all notes on a specific page (or all pages). Returns id, title, and preview.', {
|
|
31
|
+
page: z.number().optional().describe('Page number to filter. Omit for all pages.'),
|
|
32
|
+
}, async ({ page }) => {
|
|
33
|
+
try {
|
|
34
|
+
const params = {};
|
|
35
|
+
if (page !== undefined)
|
|
36
|
+
params.page = String(page);
|
|
37
|
+
const result = await apiGet('/api/mcp/notes', params);
|
|
38
|
+
const notes = result.notes;
|
|
39
|
+
if (notes.length === 0) {
|
|
40
|
+
const scope = page !== undefined ? `page ${page}` : 'any page';
|
|
41
|
+
return { content: [{ type: 'text', text: `No notes found on ${scope}.` }] };
|
|
42
|
+
}
|
|
43
|
+
const header = page !== undefined
|
|
44
|
+
? `Notes on page ${page} (${notes.length}):`
|
|
45
|
+
: `All notes (${notes.length}):`;
|
|
46
|
+
const lines = notes.map(formatSummary);
|
|
47
|
+
return { content: [{ type: 'text', text: `${header}\n\n${lines.join('\n')}` }] };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
server.tool('read_note', 'Read the full content of a specific note by its ID.', {
|
|
54
|
+
id: z.string().describe('Note UUID'),
|
|
55
|
+
}, async ({ id }) => {
|
|
56
|
+
try {
|
|
57
|
+
const result = await apiGet(`/api/mcp/notes/${id}`);
|
|
58
|
+
return { content: [{ type: 'text', text: formatNote(result.note) }] };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.tool('search_notes', 'Search notes by keyword in title or content.', {
|
|
65
|
+
keyword: z.string().describe('Search keyword'),
|
|
66
|
+
page: z.number().optional().describe('Limit search to specific page'),
|
|
67
|
+
}, async ({ keyword, page }) => {
|
|
68
|
+
try {
|
|
69
|
+
const params = { search: keyword };
|
|
70
|
+
if (page !== undefined)
|
|
71
|
+
params.page = String(page);
|
|
72
|
+
const result = await apiGet('/api/mcp/notes', params);
|
|
73
|
+
const notes = result.notes;
|
|
74
|
+
if (notes.length === 0) {
|
|
75
|
+
return { content: [{ type: 'text', text: `No notes matching "${keyword}".` }] };
|
|
76
|
+
}
|
|
77
|
+
const lines = notes.map(formatSummary);
|
|
78
|
+
return { content: [{ type: 'text', text: `Found ${notes.length} note(s) matching "${keyword}":\n\n${lines.join('\n')}` }] };
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
server.tool('create_note', 'Create a new sticky note.', {
|
|
85
|
+
title: z.string().optional().describe('Note title'),
|
|
86
|
+
content: z.string().optional().describe('Note content (plain text or markdown)'),
|
|
87
|
+
color: z.string().optional().describe('Background color hex (default: #FFD700)'),
|
|
88
|
+
page: z.number().optional().describe('Page number (default: 1)'),
|
|
89
|
+
x: z.number().optional().describe('X position'),
|
|
90
|
+
y: z.number().optional().describe('Y position'),
|
|
91
|
+
}, async ({ title, content, color, page, x, y }) => {
|
|
92
|
+
try {
|
|
93
|
+
const body = {};
|
|
94
|
+
if (title)
|
|
95
|
+
body.title = title;
|
|
96
|
+
if (content)
|
|
97
|
+
body.content = content;
|
|
98
|
+
if (color)
|
|
99
|
+
body.color = color;
|
|
100
|
+
if (page)
|
|
101
|
+
body.page_id = page;
|
|
102
|
+
if (x !== undefined || y !== undefined) {
|
|
103
|
+
body.position = { x: x ?? 100, y: y ?? 100 };
|
|
104
|
+
}
|
|
105
|
+
const result = await apiPost('/api/mcp/notes', body);
|
|
106
|
+
return { content: [{ type: 'text', text: `Note created: [${result.note.id}] ${result.note.title}` }] };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
server.tool('update_note', 'Update an existing note (title, content, color, etc.).', {
|
|
113
|
+
id: z.string().describe('Note UUID'),
|
|
114
|
+
title: z.string().optional().describe('New title'),
|
|
115
|
+
content: z.string().optional().describe('New content'),
|
|
116
|
+
color: z.string().optional().describe('New background color hex'),
|
|
117
|
+
}, async ({ id, title, content, color }) => {
|
|
118
|
+
try {
|
|
119
|
+
const body = {};
|
|
120
|
+
if (title !== undefined)
|
|
121
|
+
body.title = title;
|
|
122
|
+
if (content !== undefined)
|
|
123
|
+
body.content = content;
|
|
124
|
+
if (color !== undefined)
|
|
125
|
+
body.color = color;
|
|
126
|
+
const result = await apiPut(`/api/mcp/notes/${id}`, body);
|
|
127
|
+
return { content: [{ type: 'text', text: `Note updated: [${result.note.id}] ${result.note.title}` }] };
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
server.tool('delete_note', 'Delete a note by its ID.', {
|
|
134
|
+
id: z.string().describe('Note UUID'),
|
|
135
|
+
}, async ({ id }) => {
|
|
136
|
+
try {
|
|
137
|
+
await apiDelete(`/api/mcp/notes/${id}`);
|
|
138
|
+
return { content: [{ type: 'text', text: `Note ${id} deleted.` }] };
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { apiGet } from '../lib/api-client.js';
|
|
2
|
+
export function registerPageTools(server) {
|
|
3
|
+
server.tool('list_pages', 'List all pages that have notes, with the count of notes on each page.', {}, async () => {
|
|
4
|
+
try {
|
|
5
|
+
const result = await apiGet('/api/mcp/pages');
|
|
6
|
+
if (result.pages.length === 0) {
|
|
7
|
+
return { content: [{ type: 'text', text: 'No notes found in any page.' }] };
|
|
8
|
+
}
|
|
9
|
+
const lines = result.pages.map(p => `Page ${p.page_id}: ${p.count} note(s)`);
|
|
10
|
+
return {
|
|
11
|
+
content: [{
|
|
12
|
+
type: 'text',
|
|
13
|
+
text: `Pages with notes (${result.pages.length} pages, ${result.total_notes} total notes):\n\n${lines.join('\n')}`,
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kmemo-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Kmemo sticky notes - read, create, and manage your memos from AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kmemo-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsx watch src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"sticky-notes",
|
|
21
|
+
"memo",
|
|
22
|
+
"ai",
|
|
23
|
+
"claude",
|
|
24
|
+
"gemini"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
29
|
+
"zod": "^3.25.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20",
|
|
33
|
+
"tsx": "^4.19.0",
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
}
|
|
36
|
+
}
|