@jclvsh/xmark 0.4.0 → 0.6.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 CHANGED
@@ -16,6 +16,8 @@ npx -y -p @jclvsh/xmark xmark-mcp
16
16
  npx -y -p @jclvsh/xmark xmark --help
17
17
  ```
18
18
 
19
+ Imported as a library, the package resolves only through its `exports` map (`.`, `./hosted`, `./tool-policy`, `./policy`, `./package.json`); deep imports of `dist/*` are not supported.
20
+
19
21
  ## Setup
20
22
 
21
23
  Set your xmark API key (get one at [/settings/api](https://xmark.dev/settings/api)):
@@ -32,18 +34,18 @@ export XMARK_API_URL=https://api.xmark.dev
32
34
 
33
35
  ## Tools exposed via MCP
34
36
 
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
- | `delete_bookmark` | Remove a bookmark (local-only, or also on X with `unbookmark`). |
43
- | `list_conversations` | Past chat conversations. |
44
- | `get_conversation_messages` | A conversation's message history. |
45
- | `delete_conversation` | Permanently delete a conversation. |
46
- | `get_usage` | Plan + quota usage (chat this month, syncs today, bookmark count). |
37
+ | Tool | What it does |
38
+ | --------------------------- | ------------------------------------------------------------------ |
39
+ | `search_bookmarks` | Semantic search over the user's bookmarks. |
40
+ | `chat` | Ask a natural-language question with citations. |
41
+ | `sync_bookmarks` | Pull the latest bookmarks from X. |
42
+ | `list_bookmarks` | Paginated bookmark list. |
43
+ | `get_bookmark` | Fetch a single bookmark by ID. |
44
+ | `delete_bookmark` | Remove a bookmark (local-only, or also on X with `unbookmark`). |
45
+ | `list_conversations` | Past chat conversations. |
46
+ | `get_conversation_messages` | A conversation's message history. |
47
+ | `delete_conversation` | Permanently delete a conversation. |
48
+ | `get_usage` | Plan + quota usage (chat this month, syncs today, bookmark count). |
47
49
 
48
50
  ## CLI commands
49
51
 
package/dist/client.d.ts CHANGED
@@ -2,6 +2,8 @@ import type { Paginated } from "./types.js";
2
2
  export declare class XmarkClient {
3
3
  private apiKey;
4
4
  private baseUrl;
5
+ /** True when the base URL was injected; the network-error hint then names no URL or env var. */
6
+ private hasInjectedBaseUrl;
5
7
  constructor(apiKey?: string, baseUrl?: string);
6
8
  get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
7
9
  /**
@@ -12,8 +14,15 @@ export declare class XmarkClient {
12
14
  post<T>(path: string, body?: unknown): Promise<T>;
13
15
  delete(path: string): Promise<void>;
14
16
  private request;
15
- /** fetch with auth + UA + timeout, translating timeouts and network failures to readable errors. */
16
- private fetchWithGuards;
17
+ /** Read the `{ success, data }` envelope, throwing the api's error when `success` is false. */
18
+ private unwrap;
19
+ /** Read a JSON body; a timeout while the body streams gets the same message as one during fetch. */
20
+ private readJson;
21
+ /**
22
+ * fetch with auth + UA + timeout, translating timeouts and network failures to readable
23
+ * errors and a non-2xx response to the api's error.
24
+ */
25
+ private send;
17
26
  /**
18
27
  * Build the fleet-standard `[CODE] message (field: x)` error, the format
19
28
  * cli-utils' exitCodeForMessage / parseErrorMessage recover structurally.
package/dist/client.js CHANGED
@@ -1,16 +1,22 @@
1
+ import { createRequire } from "node:module";
1
2
  const REQUEST_TIMEOUT_MS = 30_000;
2
3
  const DEFAULT_BASE_URL = "https://api.xmark.dev";
3
- const USER_AGENT = "xmark-mcp/0.4.0";
4
+ // Read from package.json (as index.ts does) so the user agent always carries the released version.
5
+ const { version } = createRequire(import.meta.url)("../package.json");
6
+ const USER_AGENT = `xmark-mcp/${version}`;
4
7
  export class XmarkClient {
5
8
  apiKey;
6
9
  baseUrl;
10
+ /** True when the base URL was injected; the network-error hint then names no URL or env var. */
11
+ hasInjectedBaseUrl;
7
12
  constructor(apiKey, baseUrl) {
8
13
  const key = apiKey ?? process.env.XMARK_API_KEY;
9
14
  if (!key) {
10
15
  throw new Error("XMARK_API_KEY environment variable is required. Get one at https://xmark.dev/settings/api");
11
16
  }
12
17
  this.apiKey = key;
13
- this.baseUrl = (baseUrl ?? process.env.XMARK_API_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
18
+ this.hasInjectedBaseUrl = Boolean(baseUrl);
19
+ this.baseUrl = (baseUrl || process.env.XMARK_API_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
14
20
  }
15
21
  async get(path, params) {
16
22
  const url = new URL(`${this.baseUrl}${path}`);
@@ -34,11 +40,8 @@ export class XmarkClient {
34
40
  url.searchParams.set(k, String(v));
35
41
  }
36
42
  }
37
- const response = await this.fetchWithGuards(url.toString(), { method: "GET" });
38
- if (!response.ok) {
39
- await this.handleError(response);
40
- }
41
- const json = (await response.json());
43
+ const response = await this.send(url.toString(), { method: "GET" });
44
+ const json = await this.readJson(response);
42
45
  if (!json.success) {
43
46
  throw this.apiError(json.error);
44
47
  }
@@ -52,26 +55,39 @@ export class XmarkClient {
52
55
  });
53
56
  }
54
57
  async delete(path) {
55
- const response = await this.fetchWithGuards(`${this.baseUrl}${path}`, { method: "DELETE" });
56
- if (!response.ok && response.status !== 204) {
57
- await this.handleError(response);
58
- }
58
+ await this.send(`${this.baseUrl}${path}`, { method: "DELETE" });
59
59
  }
60
60
  async request(url, init) {
61
- const response = await this.fetchWithGuards(url, init);
62
- if (!response.ok) {
63
- await this.handleError(response);
64
- }
65
- const json = (await response.json());
61
+ return this.unwrap(await this.send(url, init));
62
+ }
63
+ /** Read the `{ success, data }` envelope, throwing the api's error when `success` is false. */
64
+ async unwrap(response) {
65
+ const json = await this.readJson(response);
66
66
  if (!json.success) {
67
67
  throw this.apiError(json.error);
68
68
  }
69
69
  return json.data;
70
70
  }
71
- /** fetch with auth + UA + timeout, translating timeouts and network failures to readable errors. */
72
- async fetchWithGuards(url, init) {
71
+ /** Read a JSON body; a timeout while the body streams gets the same message as one during fetch. */
72
+ async readJson(response) {
73
73
  try {
74
- return await fetch(url, {
74
+ return (await response.json());
75
+ }
76
+ catch (error) {
77
+ if (error instanceof Error && error.name === "TimeoutError") {
78
+ throw new Error("request timed out after 30s");
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+ /**
84
+ * fetch with auth + UA + timeout, translating timeouts and network failures to readable
85
+ * errors and a non-2xx response to the api's error.
86
+ */
87
+ async send(url, init) {
88
+ let response;
89
+ try {
90
+ response = await fetch(url, {
75
91
  ...init,
76
92
  headers: {
77
93
  ...(init.headers || {}),
@@ -86,10 +102,16 @@ export class XmarkClient {
86
102
  throw new Error("request timed out after 30s");
87
103
  }
88
104
  if (error instanceof TypeError) {
89
- throw new Error(`network error: ${error.message}. check XMARK_API_URL and your connection`);
105
+ throw new Error(this.hasInjectedBaseUrl
106
+ ? `network error reaching the api: ${error.message}`
107
+ : `network error: ${error.message}. check XMARK_API_URL and your connection`);
90
108
  }
91
109
  throw error;
92
110
  }
111
+ if (!response.ok) {
112
+ await this.handleError(response);
113
+ }
114
+ return response;
93
115
  }
94
116
  /**
95
117
  * Build the fleet-standard `[CODE] message (field: x)` error, the format
@@ -0,0 +1,10 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Serve one MCP request over HTTP. LOCKSTEP across every apps/<app>/packages/mcp.
4
+ *
5
+ * Stateless: a fresh transport per request, no sessions, JSON responses - so it runs on
6
+ * serverless functions where consecutive requests from one client can land on different
7
+ * instances. Only POST is meaningful; the host must not route GET here (a stateless GET
8
+ * opens an event stream that never ends).
9
+ */
10
+ export declare function serveStatelessMcp(server: McpServer, request: Request): Promise<Response>;
@@ -0,0 +1,17 @@
1
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
+ /**
3
+ * Serve one MCP request over HTTP. LOCKSTEP across every apps/<app>/packages/mcp.
4
+ *
5
+ * Stateless: a fresh transport per request, no sessions, JSON responses - so it runs on
6
+ * serverless functions where consecutive requests from one client can land on different
7
+ * instances. Only POST is meaningful; the host must not route GET here (a stateless GET
8
+ * opens an event stream that never ends).
9
+ */
10
+ export async function serveStatelessMcp(server, request) {
11
+ const transport = new WebStandardStreamableHTTPServerTransport({
12
+ sessionIdGenerator: undefined,
13
+ enableJsonResponse: true,
14
+ });
15
+ await server.connect(transport);
16
+ return transport.handleRequest(request);
17
+ }
@@ -0,0 +1,10 @@
1
+ export interface HostedMcpOptions {
2
+ /** The caller's xmark API key. The host authenticates it before calling in. */
3
+ apiKey: string;
4
+ /** REST base URL the tools call, e.g. https://api.xmark.dev */
5
+ baseUrl: string;
6
+ /** Reported as serverInfo.version */
7
+ version: string;
8
+ }
9
+ /** One hosted MCP request: a per-request client and server over the stateless transport. */
10
+ export declare function handleHostedMcpRequest(request: Request, options: HostedMcpOptions): Promise<Response>;
package/dist/hosted.js ADDED
@@ -0,0 +1,9 @@
1
+ import { XmarkClient } from "./client.js";
2
+ import { serveStatelessMcp } from "./hosted-transport.js";
3
+ import { createXmarkServer } from "./server.js";
4
+ /** One hosted MCP request: a per-request client and server over the stateless transport. */
5
+ export function handleHostedMcpRequest(request, options) {
6
+ const client = new XmarkClient(options.apiKey, options.baseUrl);
7
+ const server = createXmarkServer({ client, version: options.version, profile: "hosted" });
8
+ return serveStatelessMcp(server, request);
9
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A content-derived Idempotency-Key for create calls.
3
+ *
4
+ * The REST API dedupes a request that repeats a key within 24 hours (per key + endpoint)
5
+ * and answers with the original response plus `Idempotent-Replayed: true`. Deriving the
6
+ * key from the arguments means a retry after a timeout - by an agent, or by a person
7
+ * pressing "try again" in an MCP client - returns the record that was already created
8
+ * instead of creating a second one.
9
+ */
10
+ export declare function contentIdempotencyKey(args: unknown): Promise<string>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A content-derived Idempotency-Key for create calls.
3
+ *
4
+ * The REST API dedupes a request that repeats a key within 24 hours (per key + endpoint)
5
+ * and answers with the original response plus `Idempotent-Replayed: true`. Deriving the
6
+ * key from the arguments means a retry after a timeout - by an agent, or by a person
7
+ * pressing "try again" in an MCP client - returns the record that was already created
8
+ * instead of creating a second one.
9
+ */
10
+ function stable(value) {
11
+ if (Array.isArray(value))
12
+ return value.map(stable);
13
+ if (value !== null && typeof value === "object") {
14
+ const source = value;
15
+ const sorted = {};
16
+ for (const key of Object.keys(source).sort()) {
17
+ if (source[key] !== undefined)
18
+ sorted[key] = stable(source[key]);
19
+ }
20
+ return sorted;
21
+ }
22
+ return value;
23
+ }
24
+ export async function contentIdempotencyKey(args) {
25
+ const bytes = new TextEncoder().encode(JSON.stringify(stable(args)));
26
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
27
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
28
+ }
package/dist/index.js CHANGED
@@ -1,17 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
4
  import { XmarkClient } from "./client.js";
6
- import { registerBookmarkTools } from "./tools/bookmarks.js";
7
- import { registerChatTools } from "./tools/chat.js";
8
- import { registerUsageTools } from "./tools/usage.js";
5
+ import { createXmarkServer } from "./server.js";
9
6
  const requireFromHere = createRequire(import.meta.url);
10
7
  const pkg = requireFromHere("../package.json");
11
- const server = new McpServer({
12
- name: "xmark",
13
- version: pkg.version,
14
- });
15
8
  // Lazy client: defer the XmarkClient constructor (which throws when
16
9
  // XMARK_API_KEY is missing) until a tool invocation actually issues an HTTP
17
10
  // call. MCP discovery (initialize, tools/list) never touches the API, so the
@@ -26,8 +19,6 @@ const client = new Proxy({}, {
26
19
  return typeof value === "function" ? value.bind(realClient) : value;
27
20
  },
28
21
  });
29
- registerBookmarkTools(server, client);
30
- registerChatTools(server, client);
31
- registerUsageTools(server, client);
22
+ const server = createXmarkServer({ client, version: pkg.version, profile: "full" });
32
23
  const transport = new StdioServerTransport();
33
24
  await server.connect(transport);
@@ -0,0 +1,36 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Generic tool-policy machinery. LOCKSTEP across every apps/<app>/packages/mcp
4
+ * (byte-identical; scripts/check-lockstep-identity.sh enforces it). The per-app
5
+ * table lives in tool-policy.ts next to this file.
6
+ *
7
+ * class:
8
+ * read - retrieves information; changes nothing.
9
+ * write - changes the app's own data, reversibly; nothing leaves the app.
10
+ * sensitive - publishes, sends, or deletes on an external platform, deletes a record
11
+ * with no user-facing restore, spends money, handles credentials, or
12
+ * switches on automatic sending or posting.
13
+ * hosted: whether the app's hosted endpoint (api.<brand>/mcp) serves the tool.
14
+ * category: the documentation grouping every docs count is computed from.
15
+ */
16
+ export type ToolClass = "read" | "write" | "sensitive";
17
+ export type ToolProfile = "full" | "hosted";
18
+ export interface ToolPolicy<Category extends string = string> {
19
+ class: ToolClass;
20
+ hosted: boolean;
21
+ category: Category;
22
+ }
23
+ export type PolicyTable<Category extends string = string> = Record<string, ToolPolicy<Category>>;
24
+ export declare function toolPolicyFor<C extends string>(table: PolicyTable<C>, name: string): ToolPolicy<C>;
25
+ export declare function isToolEnabledIn<C extends string>(table: PolicyTable<C>, name: string, profile: ToolProfile): boolean;
26
+ export declare function toolAnnotationsFor<C extends string>(table: PolicyTable<C>, name: string): {
27
+ readOnlyHint: boolean;
28
+ destructiveHint: boolean;
29
+ };
30
+ /** Tool names in a category, in table order. */
31
+ export declare function toolsInCategoryOf<C extends string>(table: PolicyTable<C>, category: C): string[];
32
+ /**
33
+ * A stand-in for McpServer that every register*Tools(server, client) function can be
34
+ * handed. It is the one place a tool is filtered by profile and given its annotations.
35
+ */
36
+ export declare function createPolicyRegistrar<C extends string>(server: McpServer, table: PolicyTable<C>, profile: ToolProfile): McpServer;
package/dist/policy.js ADDED
@@ -0,0 +1,33 @@
1
+ export function toolPolicyFor(table, name) {
2
+ const policy = table[name];
3
+ if (!policy) {
4
+ throw new Error(`tool "${name}" has no entry in tool-policy.ts - classify it before registering`);
5
+ }
6
+ return policy;
7
+ }
8
+ export function isToolEnabledIn(table, name, profile) {
9
+ const { hosted } = toolPolicyFor(table, name);
10
+ return profile === "full" || hosted;
11
+ }
12
+ export function toolAnnotationsFor(table, name) {
13
+ const toolClass = toolPolicyFor(table, name).class;
14
+ return { readOnlyHint: toolClass === "read", destructiveHint: toolClass === "sensitive" };
15
+ }
16
+ /** Tool names in a category, in table order. */
17
+ export function toolsInCategoryOf(table, category) {
18
+ return Object.keys(table).filter((name) => table[name].category === category);
19
+ }
20
+ /**
21
+ * A stand-in for McpServer that every register*Tools(server, client) function can be
22
+ * handed. It is the one place a tool is filtered by profile and given its annotations.
23
+ */
24
+ export function createPolicyRegistrar(server, table, profile) {
25
+ const registerAnnotated = server.tool.bind(server);
26
+ return {
27
+ tool(name, description, schema, handler) {
28
+ if (!isToolEnabledIn(table, name, profile))
29
+ return;
30
+ registerAnnotated(name, description, schema, toolAnnotationsFor(table, name), handler);
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,10 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { XmarkClient } from "./client.js";
3
+ import { type ToolProfile } from "./policy.js";
4
+ export interface CreateXmarkServerOptions {
5
+ client: XmarkClient;
6
+ version: string;
7
+ /** "full" serves every tool (stdio). "hosted" serves the tools marked hosted in tool-policy.ts. */
8
+ profile: ToolProfile;
9
+ }
10
+ export declare function createXmarkServer({ client, version, profile }: CreateXmarkServerOptions): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,14 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { createPolicyRegistrar } from "./policy.js";
3
+ import { TOOL_POLICY } from "./tool-policy.js";
4
+ import { registerBookmarkTools } from "./tools/bookmarks.js";
5
+ import { registerChatTools } from "./tools/chat.js";
6
+ import { registerUsageTools } from "./tools/usage.js";
7
+ export function createXmarkServer({ client, version, profile }) {
8
+ const server = new McpServer({ name: "xmark", version });
9
+ const registrar = createPolicyRegistrar(server, TOOL_POLICY, profile);
10
+ registerBookmarkTools(registrar, client);
11
+ registerChatTools(registrar, client);
12
+ registerUsageTools(registrar, client);
13
+ return server;
14
+ }
@@ -0,0 +1,12 @@
1
+ import { type PolicyTable, type ToolClass, type ToolPolicy, type ToolProfile } from "./policy.js";
2
+ export type { ToolClass, ToolPolicy, ToolProfile };
3
+ export declare const TOOL_CATEGORIES: readonly ["bookmarks", "chat", "usage"];
4
+ export type ToolCategory = (typeof TOOL_CATEGORIES)[number];
5
+ export declare const TOOL_POLICY: PolicyTable<ToolCategory>;
6
+ export declare const toolPolicy: (name: string) => ToolPolicy<"usage" | "bookmarks" | "chat">;
7
+ export declare const isToolEnabled: (name: string, profile: ToolProfile) => boolean;
8
+ export declare const toolAnnotations: (name: string) => {
9
+ readOnlyHint: boolean;
10
+ destructiveHint: boolean;
11
+ };
12
+ export declare const toolsInCategory: (category: ToolCategory) => string[];
@@ -0,0 +1,29 @@
1
+ import { isToolEnabledIn, toolAnnotationsFor, toolPolicyFor, toolsInCategoryOf, } from "./policy.js";
2
+ export const TOOL_CATEGORIES = ["bookmarks", "chat", "usage"];
3
+ const hosted = (category, toolClass) => ({
4
+ class: toolClass,
5
+ hosted: true,
6
+ category,
7
+ });
8
+ export const TOOL_POLICY = {
9
+ // bookmarks
10
+ list_bookmarks: hosted("bookmarks", "read"),
11
+ get_bookmark: hosted("bookmarks", "read"),
12
+ search_bookmarks: hosted("bookmarks", "read"),
13
+ sync_bookmarks: hosted("bookmarks", "write"),
14
+ // unbookmark=true removes the bookmark on X
15
+ delete_bookmark: hosted("bookmarks", "sensitive"),
16
+ // chat
17
+ chat: hosted("chat", "write"),
18
+ list_conversations: hosted("chat", "read"),
19
+ get_conversation_messages: hosted("chat", "read"),
20
+ // permanent delete
21
+ delete_conversation: hosted("chat", "sensitive"),
22
+ // usage
23
+ get_usage: hosted("usage", "read"),
24
+ get_changelog: hosted("usage", "read"),
25
+ };
26
+ export const toolPolicy = (name) => toolPolicyFor(TOOL_POLICY, name);
27
+ export const isToolEnabled = (name, profile) => isToolEnabledIn(TOOL_POLICY, name, profile);
28
+ export const toolAnnotations = (name) => toolAnnotationsFor(TOOL_POLICY, name);
29
+ export const toolsInCategory = (category) => toolsInCategoryOf(TOOL_POLICY, category);
@@ -73,7 +73,9 @@ export function registerBookmarkTools(server, client) {
73
73
  content: [
74
74
  {
75
75
  type: "text",
76
- text: unbookmark ? "Bookmark removed from xmark and X." : "Bookmark removed from xmark (local only).",
76
+ text: unbookmark
77
+ ? "Bookmark removed from xmark and X."
78
+ : "Bookmark removed from xmark (local only).",
77
79
  },
78
80
  ],
79
81
  };
@@ -3,11 +3,7 @@ import { formatMcpError } from "../cli-utils.js";
3
3
  export function registerChatTools(server, client) {
4
4
  server.tool("chat", "Ask a natural-language question about the user's X bookmarks. Returns an answer with citations back to the original tweets. Free tier includes 20 chat messages per month.", {
5
5
  question: z.string().min(1).describe("The question to ask"),
6
- conversationId: z
7
- .string()
8
- .uuid()
9
- .optional()
10
- .describe("Continue an existing conversation by ID"),
6
+ conversationId: z.string().uuid().optional().describe("Continue an existing conversation by ID"),
11
7
  maxCitations: z
12
8
  .number()
13
9
  .int()
@@ -11,4 +11,13 @@ export function registerUsageTools(server, client) {
11
11
  return formatMcpError(error);
12
12
  }
13
13
  });
14
+ server.tool("get_changelog", "machine-readable release history (public) - version, date, type, summary, migration notes on breaking entries. the X-Notice header's structured backstory.", {}, async () => {
15
+ try {
16
+ const result = await client.get("/changelog");
17
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
18
+ }
19
+ catch (error) {
20
+ return formatMcpError(error);
21
+ }
22
+ });
14
23
  }
package/package.json CHANGED
@@ -1,10 +1,29 @@
1
1
  {
2
2
  "name": "@jclvsh/xmark",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
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
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./hosted": {
14
+ "types": "./dist/hosted.d.ts",
15
+ "default": "./dist/hosted.js"
16
+ },
17
+ "./policy": {
18
+ "types": "./dist/policy.d.ts",
19
+ "default": "./dist/policy.js"
20
+ },
21
+ "./tool-policy": {
22
+ "types": "./dist/tool-policy.d.ts",
23
+ "default": "./dist/tool-policy.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
8
27
  "bin": {
9
28
  "xmark-mcp": "dist/index.js",
10
29
  "xmark": "dist/cli.js"
@@ -15,8 +34,11 @@
15
34
  ],
16
35
  "scripts": {
17
36
  "build": "tsc",
37
+ "prepare": "tsc",
18
38
  "prepublishOnly": "npm run build",
19
- "start": "node dist/index.js"
39
+ "start": "node dist/index.js",
40
+ "typecheck": "tsc -p tsconfig.test.json",
41
+ "test": "vitest run"
20
42
  },
21
43
  "keywords": [
22
44
  "mcp",
@@ -30,13 +52,14 @@
30
52
  "xmark"
31
53
  ],
32
54
  "dependencies": {
33
- "@modelcontextprotocol/sdk": "^1.11.0",
55
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
56
  "commander": "^14.0.3",
35
57
  "zod": "^3.24.0"
36
58
  },
37
59
  "devDependencies": {
38
60
  "@types/node": "^22.0.0",
39
- "typescript": "^5.7.0"
61
+ "typescript": "^5.7.0",
62
+ "vitest": "^4.0.18"
40
63
  },
41
64
  "engines": {
42
65
  "node": ">=18"