@jclvsh/xmark 0.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 +67 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +68 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/tools/bookmarks.d.ts +3 -0
- package/dist/tools/bookmarks.js +48 -0
- package/dist/tools/chat.d.ts +3 -0
- package/dist/tools/chat.js +44 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @jclvsh/xmark
|
|
2
|
+
|
|
3
|
+
MCP server and CLI for the [xmark](https://www.xmark.chat) API — chat with your X (Twitter) bookmarks from inside Claude Code, Cursor, or any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
## What is xmark?
|
|
6
|
+
|
|
7
|
+
xmark syncs your X bookmarks, embeds them with OpenAI, and lets you query them in natural language with citations back to the original tweets. This package wraps the [xmark REST API](https://api.xmark.chat/openapi.json) as an MCP server (for AI clients) and a CLI (for terminal use).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Run the MCP server (stdio transport)
|
|
13
|
+
npx -y -p @jclvsh/xmark xmark-mcp
|
|
14
|
+
|
|
15
|
+
# Or use the CLI directly
|
|
16
|
+
npx -y -p @jclvsh/xmark xmark --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
Set your xmark API key (get one at [/settings/api](https://www.xmark.chat/settings/api)):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export XMARK_API_KEY=xm_live_...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Optionally override the API host (default is `https://api.xmark.chat`):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export XMARK_API_URL=https://api.xmark.chat
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Tools exposed via MCP
|
|
34
|
+
|
|
35
|
+
| Tool | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `search_bookmarks` | Semantic search over the user's bookmarks. |
|
|
38
|
+
| `chat` | Ask a natural-language question with citations. |
|
|
39
|
+
| `sync_bookmarks` | Pull the latest bookmarks from X. |
|
|
40
|
+
| `list_bookmarks` | Paginated bookmark list. |
|
|
41
|
+
| `get_bookmark` | Fetch a single bookmark by ID. |
|
|
42
|
+
| `list_conversations` | Past chat conversations. |
|
|
43
|
+
|
|
44
|
+
## CLI commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
xmark search "what did I save about Claude?"
|
|
48
|
+
xmark chat "summarize what I saved this month about agents"
|
|
49
|
+
xmark sync
|
|
50
|
+
xmark list --page 1 --page-size 20
|
|
51
|
+
xmark conversations
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Pricing
|
|
55
|
+
|
|
56
|
+
xmark is $5/month flat. The same subscription covers the web app, CLI, MCP server, and developer REST API — no separate API tier.
|
|
57
|
+
|
|
58
|
+
## Links
|
|
59
|
+
|
|
60
|
+
- Homepage: https://www.xmark.chat
|
|
61
|
+
- API reference: https://www.xmark.chat/docs
|
|
62
|
+
- OpenAPI spec: https://api.xmark.chat/openapi.json
|
|
63
|
+
- llms.txt: https://api.xmark.chat/llms.txt
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { XmarkClient } from "./client.js";
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name("xmark")
|
|
7
|
+
.description("xmark CLI — chat with your X bookmarks from the terminal")
|
|
8
|
+
.version("0.1.0");
|
|
9
|
+
function client() {
|
|
10
|
+
return new XmarkClient();
|
|
11
|
+
}
|
|
12
|
+
program
|
|
13
|
+
.command("search <query>")
|
|
14
|
+
.description("Semantic search over your bookmarks ($0.04/call for agents)")
|
|
15
|
+
.option("-l, --limit <n>", "max results", "10")
|
|
16
|
+
.action(async (query, opts) => {
|
|
17
|
+
const result = await client().post("/bookmarks/search", {
|
|
18
|
+
query,
|
|
19
|
+
limit: parseInt(opts.limit, 10),
|
|
20
|
+
});
|
|
21
|
+
console.log(JSON.stringify(result, null, 2));
|
|
22
|
+
});
|
|
23
|
+
program
|
|
24
|
+
.command("chat <question...>")
|
|
25
|
+
.description("Ask a question about your bookmarks ($0.20/call for agents)")
|
|
26
|
+
.option("-c, --conversation <id>", "continue an existing conversation")
|
|
27
|
+
.action(async (questionParts, opts) => {
|
|
28
|
+
const question = questionParts.join(" ");
|
|
29
|
+
const result = await client().post("/chat", {
|
|
30
|
+
question,
|
|
31
|
+
...(opts.conversation && { conversationId: opts.conversation }),
|
|
32
|
+
});
|
|
33
|
+
console.log(result.answer);
|
|
34
|
+
console.log("\nCitations:");
|
|
35
|
+
for (const c of result.citations) {
|
|
36
|
+
console.log(` @${c.author} — ${c.snippet}\n ${c.tweet_url}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command("sync")
|
|
41
|
+
.description("Trigger a fresh sync from X")
|
|
42
|
+
.action(async () => {
|
|
43
|
+
const result = await client().post("/bookmarks/sync");
|
|
44
|
+
console.log(`Synced ${result.count} bookmarks (${result.newCount} new) in ${result.durationMs}ms.`);
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command("list")
|
|
48
|
+
.description("List your bookmarks")
|
|
49
|
+
.option("-p, --page <n>", "page number", "1")
|
|
50
|
+
.option("-s, --page-size <n>", "items per page", "50")
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
const result = await client().get("/bookmarks", {
|
|
53
|
+
page: parseInt(opts.page, 10),
|
|
54
|
+
pageSize: parseInt(opts.pageSize, 10),
|
|
55
|
+
});
|
|
56
|
+
console.log(JSON.stringify(result, null, 2));
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command("conversations")
|
|
60
|
+
.description("List your past chat conversations")
|
|
61
|
+
.action(async () => {
|
|
62
|
+
const result = await client().get("/conversations");
|
|
63
|
+
console.log(JSON.stringify(result, null, 2));
|
|
64
|
+
});
|
|
65
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
66
|
+
console.error(err instanceof Error ? err.message : err);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class XmarkClient {
|
|
2
|
+
private apiKey;
|
|
3
|
+
private baseUrl;
|
|
4
|
+
constructor(apiKey?: string, baseUrl?: string);
|
|
5
|
+
get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
6
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
7
|
+
delete(path: string): Promise<void>;
|
|
8
|
+
private request;
|
|
9
|
+
private handleError;
|
|
10
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
2
|
+
const DEFAULT_BASE_URL = "https://api.xmark.chat";
|
|
3
|
+
export class XmarkClient {
|
|
4
|
+
apiKey;
|
|
5
|
+
baseUrl;
|
|
6
|
+
constructor(apiKey, baseUrl) {
|
|
7
|
+
const key = apiKey ?? process.env.XMARK_API_KEY;
|
|
8
|
+
if (!key) {
|
|
9
|
+
throw new Error("XMARK_API_KEY environment variable is required. Get one at https://www.xmark.chat/settings/api");
|
|
10
|
+
}
|
|
11
|
+
this.apiKey = key;
|
|
12
|
+
this.baseUrl = (baseUrl ?? process.env.XMARK_API_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
13
|
+
}
|
|
14
|
+
async get(path, params) {
|
|
15
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
16
|
+
if (params) {
|
|
17
|
+
for (const [k, v] of Object.entries(params)) {
|
|
18
|
+
if (v !== undefined && v !== "")
|
|
19
|
+
url.searchParams.set(k, String(v));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return this.request(url.toString(), { method: "GET" });
|
|
23
|
+
}
|
|
24
|
+
async post(path, body) {
|
|
25
|
+
return this.request(`${this.baseUrl}${path}`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async delete(path) {
|
|
32
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
33
|
+
method: "DELETE",
|
|
34
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
35
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok && response.status !== 204) {
|
|
38
|
+
await this.handleError(response);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async request(url, init) {
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
...init,
|
|
44
|
+
headers: {
|
|
45
|
+
...init.headers,
|
|
46
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
47
|
+
"User-Agent": "xmark-mcp/0.1.0",
|
|
48
|
+
},
|
|
49
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
await this.handleError(response);
|
|
53
|
+
}
|
|
54
|
+
const json = (await response.json());
|
|
55
|
+
if (!json.success) {
|
|
56
|
+
throw new Error(`${json.error.code}: ${json.error.message}`);
|
|
57
|
+
}
|
|
58
|
+
return json.data;
|
|
59
|
+
}
|
|
60
|
+
async handleError(response) {
|
|
61
|
+
let detail = "";
|
|
62
|
+
try {
|
|
63
|
+
const body = (await response.json());
|
|
64
|
+
if (body?.error) {
|
|
65
|
+
detail = `${body.error.code}: ${body.error.message}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
detail = await response.text().catch(() => "");
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`xmark API ${response.status} ${response.statusText}${detail ? ` — ${detail}` : ""}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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 { XmarkClient } from "./client.js";
|
|
5
|
+
import { registerBookmarkTools } from "./tools/bookmarks.js";
|
|
6
|
+
import { registerChatTools } from "./tools/chat.js";
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "xmark",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
});
|
|
11
|
+
const client = new XmarkClient();
|
|
12
|
+
registerBookmarkTools(server, client);
|
|
13
|
+
registerChatTools(server, client);
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
15
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerBookmarkTools(server, client) {
|
|
3
|
+
server.tool("list_bookmarks", "List the authenticated user's X bookmarks. Paginated.", {
|
|
4
|
+
page: z.number().int().min(1).default(1).describe("Page number (1-indexed)"),
|
|
5
|
+
pageSize: z.number().int().min(1).max(100).default(50).describe("Items per page (max 100)"),
|
|
6
|
+
}, async ({ page, pageSize }) => {
|
|
7
|
+
const result = await client.get("/bookmarks", {
|
|
8
|
+
page,
|
|
9
|
+
pageSize,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: JSON.stringify(result, null, 2),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
server.tool("get_bookmark", "Fetch a single bookmark by ID.", {
|
|
21
|
+
id: z.string().uuid().describe("Bookmark UUID"),
|
|
22
|
+
}, async ({ id }) => {
|
|
23
|
+
const result = await client.get(`/bookmarks/${id}`);
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
server.tool("search_bookmarks", "Semantic search over the user's bookmarks. Returns ranked matches with relevance scores. Charged at $0.04/call for agents (first 50/month free).", {
|
|
29
|
+
query: z.string().min(1).describe("Natural-language search query"),
|
|
30
|
+
limit: z.number().int().min(1).max(50).default(10).describe("Max results to return"),
|
|
31
|
+
}, async ({ query, limit }) => {
|
|
32
|
+
const result = await client.post("/bookmarks/search", { query, limit });
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
server.tool("sync_bookmarks", "Trigger a fresh sync from X. Pulls latest bookmarks, embeds new ones for semantic search.", {}, async () => {
|
|
38
|
+
const result = await client.post("/bookmarks/sync");
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `Synced ${result.count} bookmarks (${result.newCount} new) in ${result.durationMs}ms.`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerChatTools(server, client) {
|
|
3
|
+
server.tool("chat", "Ask a natural-language question about the user's X bookmarks. Returns an answer with citations back to the original tweets. Charged at $0.20/call for agents.", {
|
|
4
|
+
question: z.string().min(1).describe("The question to ask"),
|
|
5
|
+
conversationId: z
|
|
6
|
+
.string()
|
|
7
|
+
.uuid()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Continue an existing conversation by ID"),
|
|
10
|
+
maxCitations: z
|
|
11
|
+
.number()
|
|
12
|
+
.int()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(20)
|
|
15
|
+
.default(5)
|
|
16
|
+
.describe("Maximum citations to include in the answer"),
|
|
17
|
+
}, async ({ question, conversationId, maxCitations }) => {
|
|
18
|
+
const result = await client.post("/chat", {
|
|
19
|
+
question,
|
|
20
|
+
...(conversationId && { conversationId }),
|
|
21
|
+
maxCitations,
|
|
22
|
+
});
|
|
23
|
+
const citations = result.citations
|
|
24
|
+
.map((c, i) => `[${i + 1}] @${c.author} — ${c.snippet}\n ${c.tweet_url}`)
|
|
25
|
+
.join("\n");
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: `${result.answer}\n\nCitations:\n${citations}\n\nConversation ID: ${result.conversation_id}`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
server.tool("list_conversations", "List the user's past chat conversations. Paginated.", {
|
|
36
|
+
page: z.number().int().min(1).default(1),
|
|
37
|
+
pageSize: z.number().int().min(1).max(100).default(50),
|
|
38
|
+
}, async ({ page, pageSize }) => {
|
|
39
|
+
const result = await client.get("/conversations", { page, pageSize });
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface ApiSuccessResponse<T> {
|
|
2
|
+
success: true;
|
|
3
|
+
data: T;
|
|
4
|
+
}
|
|
5
|
+
export interface ApiErrorResponse {
|
|
6
|
+
success: false;
|
|
7
|
+
error: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
requestId?: string;
|
|
11
|
+
field?: string;
|
|
12
|
+
details?: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
|
|
16
|
+
export interface Bookmark {
|
|
17
|
+
id: string;
|
|
18
|
+
tweet_id: string;
|
|
19
|
+
text: string;
|
|
20
|
+
author: {
|
|
21
|
+
username: string;
|
|
22
|
+
name: string;
|
|
23
|
+
id: string;
|
|
24
|
+
};
|
|
25
|
+
bookmarked_at: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
media?: unknown[];
|
|
28
|
+
topics?: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface BookmarkSearchResult {
|
|
31
|
+
bookmark: Bookmark;
|
|
32
|
+
score: number;
|
|
33
|
+
}
|
|
34
|
+
export interface ChatCitation {
|
|
35
|
+
bookmark_id: string;
|
|
36
|
+
tweet_url: string;
|
|
37
|
+
author: string;
|
|
38
|
+
snippet: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ChatResult {
|
|
41
|
+
answer: string;
|
|
42
|
+
citations: ChatCitation[];
|
|
43
|
+
conversation_id: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SyncResult {
|
|
46
|
+
count: number;
|
|
47
|
+
newCount: number;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
}
|
|
50
|
+
export interface Conversation {
|
|
51
|
+
id: string;
|
|
52
|
+
title: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
message_count: number;
|
|
55
|
+
}
|
|
56
|
+
export interface PaginationMeta {
|
|
57
|
+
page: number;
|
|
58
|
+
pageSize: number;
|
|
59
|
+
total: number;
|
|
60
|
+
totalPages: number;
|
|
61
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jclvsh/xmark",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server and CLI for the xmark API — chat with your X bookmarks from inside Claude Code, Cursor, or any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"xmark-mcp": "dist/index.js",
|
|
10
|
+
"xmark": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"start": "node dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"twitter",
|
|
25
|
+
"x",
|
|
26
|
+
"bookmarks",
|
|
27
|
+
"ai",
|
|
28
|
+
"claude",
|
|
29
|
+
"semantic-search",
|
|
30
|
+
"xmark"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
34
|
+
"commander": "^14.0.3",
|
|
35
|
+
"zod": "^3.24.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.7.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/jclvsh/xmark.git"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://www.xmark.chat"
|
|
50
|
+
}
|