1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
import { z } from "zod";
import { karakeepClient, mcpServer, turndownService } from "./shared";
import { compactBookmark, toMcpToolError } from "./utils";
// Tools
mcpServer.tool(
"search-bookmarks",
`Search for bookmarks matching a specific a query.
`,
{
query: z.string().describe(`
By default, this will do a full-text search, but you can also use qualifiers to filter the results.
You can search bookmarks using specific qualifiers. is:fav finds favorited bookmarks,
is:archived searches archived bookmarks, is:tagged finds those with tags,
is:inlist finds those in lists, and is:link, is:text, and is:media filter by bookmark type.
url:<value> searches for URL substrings, #<tag> searches for bookmarks with a specific tag,
list:<name> searches for bookmarks in a specific list given its name,
after:<date> finds bookmarks created on or after a date (YYYY-MM-DD), and before:<date> finds bookmarks created on or before a date (YYYY-MM-DD).
If you need to pass names with spaces, you can quote them with double quotes. If you want to negate a qualifier, prefix it with a minus sign.
## Examples:
### Find favorited bookmarks from 2023 that are tagged "important"
is:fav after:2023-01-01 before:2023-12-31 #important
### Find archived bookmarks that are either in "reading" list or tagged "work"
is:archived and (list:reading or #work)
### Combine text search with qualifiers
machine learning is:fav`),
limit: z
.number()
.optional()
.describe(`The number of results to return in a single query.`)
.default(10),
nextCursor: z
.string()
.optional()
.describe(
`The next cursor to use for pagination. The value for this is returned from a previous call to this tool.`,
),
},
async ({ query, limit, nextCursor }): Promise<CallToolResult> => {
const res = await karakeepClient.GET("/bookmarks/search", {
params: {
query: {
q: query,
limit: limit,
includeContent: false,
cursor: nextCursor,
},
},
});
if (!res.data) {
return toMcpToolError(res.error);
}
return {
content: [
...res.data.bookmarks.map((bookmark) => ({
type: "text" as const,
text: JSON.stringify(compactBookmark(bookmark)),
})),
{
type: "text",
text: `Next cursor: ${res.data.nextCursor ? `'${res.data.nextCursor}'` : "no more pages"}`,
},
],
};
},
);
mcpServer.tool(
"get-bookmark",
`Get a bookmark by id.`,
{
bookmarkId: z.string().describe(`The bookmarkId to get.`),
},
async ({ bookmarkId }): Promise<CallToolResult> => {
const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, {
params: {
path: {
bookmarkId,
},
query: {
includeContent: false,
},
},
});
if (res.error) {
return toMcpToolError(res.error);
}
return {
content: [
{
type: "text",
text: JSON.stringify(compactBookmark(res.data)),
},
],
};
},
);
mcpServer.tool(
"create-bookmark",
`Create a link bookmark or a text bookmark`,
{
type: z.enum(["link", "text"]).describe(`The type of bookmark to create.`),
title: z.string().optional().describe(`The title of the bookmark`),
content: z
.string()
.describe(
"If type is text, the text to be bookmarked. If the type is link, then it's the URL to be bookmarked.",
),
},
async ({ title, type, content }): Promise<CallToolResult> => {
const res = await karakeepClient.POST(`/bookmarks`, {
body: (
{
link: {
type: "link",
title,
url: content,
},
text: {
type: "text",
title,
text: content,
},
} as const
)[type],
});
if (res.error) {
return toMcpToolError(res.error);
}
return {
content: [
{
type: "text",
text: JSON.stringify(compactBookmark(res.data)),
},
],
};
},
);
mcpServer.tool(
"get-bookmark-content",
`Get the content of the bookmark in markdown`,
{
bookmarkId: z.string().describe(`The bookmarkId to get content for.`),
},
async ({ bookmarkId }): Promise<CallToolResult> => {
const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, {
params: {
path: {
bookmarkId,
},
query: {
includeContent: true,
},
},
});
if (res.error) {
return toMcpToolError(res.error);
}
let content;
if (res.data.content.type === "link") {
const htmlContent = res.data.content.htmlContent;
content = turndownService.turndown(htmlContent);
} else if (res.data.content.type === "text") {
content = res.data.content.text;
} else if (res.data.content.type === "asset") {
content = res.data.content.content;
}
return {
content: [
{
type: "text",
text: content ?? "",
},
],
};
},
);
|