@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ });
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { XmarkClient } from "../client.js";
3
+ export declare function registerBookmarkTools(server: McpServer, client: XmarkClient): void;
@@ -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,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { XmarkClient } from "../client.js";
3
+ export declare function registerChatTools(server: McpServer, client: XmarkClient): void;
@@ -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
+ }
@@ -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
+ }