@jclvsh/xmark 0.6.0 → 0.6.2

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.
@@ -8,3 +8,22 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  * opens an event stream that never ends).
9
9
  */
10
10
  export declare function serveStatelessMcp(server: McpServer, request: Request): Promise<Response>;
11
+ /**
12
+ * True when a JSON-RPC payload (one message or a batch) only asks who the server is and which
13
+ * tools it has. A host may answer these without a key, so an agent or a directory can see what
14
+ * the server does before signing up. Every message must be on the list: one tool call in a
15
+ * batch makes the whole payload need a key.
16
+ */
17
+ export declare function isDiscoveryPayload(payload: unknown): boolean;
18
+ /**
19
+ * True when the request's body is a discovery payload. Reads a clone, so the request can still
20
+ * be served afterwards, and stops at MAX_DISCOVERY_BODY_BYTES: a larger body, or a chunked one
21
+ * with no Content-Length, is never buffered whole.
22
+ */
23
+ export declare function isDiscoveryRequest(request: Request): Promise<boolean>;
24
+ /**
25
+ * The API client for a keyless discovery server: it has no key, so every use throws. Listing
26
+ * tools never touches the client; a tool call that reached this server would fail here, before
27
+ * any REST request.
28
+ */
29
+ export declare function keylessClient<Client extends object>(): Client;
@@ -15,3 +15,70 @@ export async function serveStatelessMcp(server, request) {
15
15
  await server.connect(transport);
16
16
  return transport.handleRequest(request);
17
17
  }
18
+ /** JSON-RPC methods that describe the server and change nothing. */
19
+ const DISCOVERY_METHODS = new Set(["initialize", "notifications/initialized", "ping", "tools/list"]);
20
+ /** A handshake is three messages; a longer keyless batch is not a handshake. */
21
+ const MAX_DISCOVERY_BATCH = 10;
22
+ /**
23
+ * True when a JSON-RPC payload (one message or a batch) only asks who the server is and which
24
+ * tools it has. A host may answer these without a key, so an agent or a directory can see what
25
+ * the server does before signing up. Every message must be on the list: one tool call in a
26
+ * batch makes the whole payload need a key.
27
+ */
28
+ export function isDiscoveryPayload(payload) {
29
+ const messages = Array.isArray(payload) ? payload : [payload];
30
+ if (messages.length === 0 || messages.length > MAX_DISCOVERY_BATCH)
31
+ return false;
32
+ return messages.every((message) => {
33
+ if (typeof message !== "object" || message === null)
34
+ return false;
35
+ const method = message.method;
36
+ return typeof method === "string" && DISCOVERY_METHODS.has(method);
37
+ });
38
+ }
39
+ /** A handshake fits in a few hundred bytes; a keyless body past this is not one. */
40
+ const MAX_DISCOVERY_BODY_BYTES = 16_384;
41
+ /**
42
+ * True when the request's body is a discovery payload. Reads a clone, so the request can still
43
+ * be served afterwards, and stops at MAX_DISCOVERY_BODY_BYTES: a larger body, or a chunked one
44
+ * with no Content-Length, is never buffered whole.
45
+ */
46
+ export async function isDiscoveryRequest(request) {
47
+ const stream = request.clone().body;
48
+ if (!stream)
49
+ return false;
50
+ const reader = stream.getReader();
51
+ const bytes = new Uint8Array(MAX_DISCOVERY_BODY_BYTES);
52
+ let size = 0;
53
+ try {
54
+ for (;;) {
55
+ const { done, value } = await reader.read();
56
+ if (done)
57
+ break;
58
+ if (size + value.byteLength > MAX_DISCOVERY_BODY_BYTES) {
59
+ // Not awaited: cancelling one side of a cloned body only settles once the other
60
+ // side is cancelled too, and the caller is about to answer without reading it.
61
+ void reader.cancel().catch(() => undefined);
62
+ return false;
63
+ }
64
+ bytes.set(value, size);
65
+ size += value.byteLength;
66
+ }
67
+ return isDiscoveryPayload(JSON.parse(new TextDecoder().decode(bytes.subarray(0, size))));
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * The API client for a keyless discovery server: it has no key, so every use throws. Listing
75
+ * tools never touches the client; a tool call that reached this server would fail here, before
76
+ * any REST request.
77
+ */
78
+ export function keylessClient() {
79
+ return new Proxy({}, {
80
+ get(_target, property) {
81
+ throw new Error(`an api key is required (client.${String(property)} was used without one)`);
82
+ },
83
+ });
84
+ }
package/dist/hosted.d.ts CHANGED
@@ -8,3 +8,12 @@ export interface HostedMcpOptions {
8
8
  }
9
9
  /** One hosted MCP request: a per-request client and server over the stateless transport. */
10
10
  export declare function handleHostedMcpRequest(request: Request, options: HostedMcpOptions): Promise<Response>;
11
+ /**
12
+ * One keyless discovery request (initialize, tools/list): the same hosted server and tool list,
13
+ * built on a client that cannot make a REST call. The host checks the request with
14
+ * isDiscoveryRequest first; anything else needs a key and goes to handleHostedMcpRequest.
15
+ */
16
+ export declare function handleHostedMcpDiscovery(request: Request, options: {
17
+ version: string;
18
+ }): Promise<Response>;
19
+ export { isDiscoveryPayload, isDiscoveryRequest } from "./hosted-transport.js";
package/dist/hosted.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { XmarkClient } from "./client.js";
2
- import { serveStatelessMcp } from "./hosted-transport.js";
2
+ import { keylessClient, serveStatelessMcp } from "./hosted-transport.js";
3
3
  import { createXmarkServer } from "./server.js";
4
4
  /** One hosted MCP request: a per-request client and server over the stateless transport. */
5
5
  export function handleHostedMcpRequest(request, options) {
@@ -7,3 +7,17 @@ export function handleHostedMcpRequest(request, options) {
7
7
  const server = createXmarkServer({ client, version: options.version, profile: "hosted" });
8
8
  return serveStatelessMcp(server, request);
9
9
  }
10
+ /**
11
+ * One keyless discovery request (initialize, tools/list): the same hosted server and tool list,
12
+ * built on a client that cannot make a REST call. The host checks the request with
13
+ * isDiscoveryRequest first; anything else needs a key and goes to handleHostedMcpRequest.
14
+ */
15
+ export function handleHostedMcpDiscovery(request, options) {
16
+ const server = createXmarkServer({
17
+ client: keylessClient(),
18
+ version: options.version,
19
+ profile: "hosted",
20
+ });
21
+ return serveStatelessMcp(server, request);
22
+ }
23
+ export { isDiscoveryPayload, isDiscoveryRequest } from "./hosted-transport.js";
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@jclvsh/xmark",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
+ "mcpName": "io.github.JCLVSH/xmark",
4
5
  "description": "MCP server and CLI for the xmark API — chat with your X bookmarks from inside Claude Code, Cursor, or any MCP client.",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",