@clipform/mcp-server 3.3.1 → 3.7.1

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.
@@ -3,6 +3,8 @@ import { AsyncLocalStorage } from "async_hooks";
3
3
  var MCP_ANONYMOUS_HEADER = "x-clipform-anonymous";
4
4
  var MCP_AUTH_VIA_HEADER = "x-clipform-auth-via";
5
5
  var MCP_MINT_PROOF_HEADER = "x-clipform-mcp-mint-proof";
6
+ var MCP_SESSION_PROOF_HEADER = "x-clipform-mcp-session-proof";
7
+ var MCP_SESSION_ID_HEADER = "x-clipform-mcp-session-id";
6
8
  var storage = new AsyncLocalStorage();
7
9
  function runWithMcpAuth(ctx, fn) {
8
10
  return storage.run(ctx, fn);
@@ -20,8 +22,10 @@ export {
20
22
  MCP_ANONYMOUS_HEADER,
21
23
  MCP_AUTH_VIA_HEADER,
22
24
  MCP_MINT_PROOF_HEADER,
25
+ MCP_SESSION_PROOF_HEADER,
26
+ MCP_SESSION_ID_HEADER,
23
27
  runWithMcpAuth,
24
28
  getMcpAuth,
25
29
  runWithMcpTool
26
30
  };
27
- //# sourceMappingURL=chunk-A5CFGL27.js.map
31
+ //# sourceMappingURL=chunk-VPF6MLKM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/auth-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Identity of the end-user a given MCP request is acting on behalf of.\n *\n * Populated by the in-process host (apps/api) after validating an OAuth\n * Bearer on /mcp. The MCP tool layer reads it to decide which workspace\n * forms should land in and which plan tier gates apply.\n *\n * NOT populated when the server runs as a local stdio process (npx) - in\n * that mode tools fall back to edit-token based auth.\n */\nexport interface McpAuthContext {\n user_id: string;\n workspace_id: string;\n scopes: string[];\n tool_name?: string;\n api_key?: string;\n // Set when the human behind this tool call is an anonymous Supabase\n // session (apps/api's app-chat lane, #2983/#2984) - api-client.ts's\n // getAuthHeaders reads it to mark the outgoing bot-key HTTP request so the\n // API can propagate the restriction even though the request itself\n // authenticates with an API key, not a JWT.\n is_anonymous?: boolean;\n // Set on the OAuth branch of apps/api's mcp/handler.ts when this request's\n // bot API key was resolved on behalf of an OAuth-connected user (#3716) -\n // api-client.ts's getAuthHeaders reads it to mark the outgoing request so\n // /v1/me can report auth_mode \"oauth\" instead of the generic \"api_key\" the\n // bot-key lane would otherwise get, even though the request itself\n // authenticates with an API key, not an OAuth token.\n auth_via?: \"oauth\";\n // #3330 review fix (blocker 1): an HMAC proof (apps/api's mcp-mint-proof.ts)\n // that THIS exact request's #1415 anon lane freshly minted a brand-new anon\n // identity for `workspace_id` - set only on the mint branch of\n // resolveAnonymousMcpAuth, never when auth was resolved via an inbound\n // form_id (even the caller's own). Lets create-form.ts safely offer a\n // same-UID claim link without ever deriving \"who to bind it to\" from a\n // body-supplied id, which would let a batch smuggling another tenant's\n // form_id alongside a create_form call mint a claim token for THAT\n // tenant's identity instead of the caller's own.\n mcp_mint_proof?: string;\n // #4090: sibling of mcp_mint_proof, set when the anon lane REUSED a\n // server-verified session binding (#4652) instead of minting. HMAC-bound\n // to the bound {workspace, uid}; lets create-form.ts re-surface the claim\n // link on later forms in the session.\n mcp_session_proof?: string;\n // #3822: set ONLY on the OAuth branch of apps/api's mcp/handler.ts. Lets a\n // single tool call target another workspace the connected user belongs to\n // (per-call, never per-request - see the #3330 note in the package\n // CLAUDE.md). The host re-checks membership on every call and returns that\n // workspace's own bot key, or null to mean \"act in the default workspace\n // unchanged\" (target is the default, or a form_id the user can't reach -\n // the API then answers exactly as it did before targeting existed). Anon + stdio contexts never carry it, so they\n // stay pinned to one workspace.\n resolveWorkspace?: (target: {\n workspace_id?: string;\n form_id?: string;\n }) => Promise<{ workspace_id: string; workspace_name: string; api_key: string } | null>;\n // #3822: the workspaces the connected user may target (same set\n // resolveWorkspace accepts). OAuth lane only.\n listWorkspaces?: () => Promise<{ id: string; name: string }[]>;\n // #3822: the workspace picked at authorize time - what a call with no\n // `workspace_id` acts in. OAuth lane only (workspace_id above is swapped\n // per call when a target is resolved, this never is).\n default_workspace_id?: string;\n // #4657: set ONLY by apps/api's mcp/handler.ts for an anonymous request made\n // entirely of `anon_no_workspace` tools (get_guide / get_workflow /\n // list_assets). That lane has NO credential of any kind: the static content\n // is served in-process through this loader instead of an HTTP callApi, so no\n // secret or key ever sits in the anonymous context. Tools use it when\n // present and callApi otherwise (stdio / published behaviour unchanged).\n // #4657: decoded PostHog MCP `$session_id` from the inbound Mcp-Session-Id -\n // ANALYTICS ONLY (stamped on funnel events). api-client.ts forwards it as\n // MCP_SESSION_ID_HEADER; nothing about identity or access derives from it.\n mcp_session_id?: string;\n loadInternalContent?: (\n kind: \"guides\" | \"assets\",\n params?: { type?: string }\n ) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Marker header the in-process MCP tool layer sets on its outgoing API\n * requests (api-client.ts) when `is_anonymous` is true, so an anonymous\n * app-chat session can't launder itself into an unrestricted API-key caller\n * by asking the bot to publish/mint credentials on its behalf. apps/api's\n * authMiddleware / workspaceAccessMiddleware / requireWorkspaceWriteAuth\n * read this on the API-key path and set isAnonymous accordingly - trusting\n * it is safe because it can only RESTRICT a caller: a real integration that\n * sets this header on its own API-key requests only denies itself, it can\n * never use it to gain anonymous-JWT privileges it doesn't already have.\n */\nexport const MCP_ANONYMOUS_HEADER = \"x-clipform-anonymous\";\n\n/**\n * Marker header the in-process MCP tool layer sets on its outgoing API\n * requests (api-client.ts) when `auth_via` is \"oauth\", so /v1/me can report\n * `auth_mode: \"oauth\"` for an OAuth-connected user instead of the generic\n * \"api_key\" the bot-key lane would otherwise get (#3716). Label-only: it\n * changes nothing but the reported string. Trusting it from the wire is\n * safe because a direct API-key caller who sets this header on their own\n * request only mislabels their own whoami output - they gain no privilege,\n * exactly the same reasoning as MCP_ANONYMOUS_HEADER above (except that\n * header can only restrict, and this one can only relabel).\n */\nexport const MCP_AUTH_VIA_HEADER = \"x-clipform-auth-via\";\n\n/**\n * Carries `mcp_mint_proof` on the same outgoing bot-key request. Unlike\n * MCP_ANONYMOUS_HEADER (a boolean that can only ever RESTRICT), this header's\n * VALUE matters - it names an identity a claim token gets bound to - so its\n * value is HMAC-signed (mcp-mint-proof.ts) rather than trusted raw: a direct\n * external caller reaching `POST /v1/forms` with any valid workspace API key\n * could otherwise hand-set an unsigned \"mint uid\" header to an arbitrary\n * victim's uid themselves (#3330 review).\n */\nexport const MCP_MINT_PROOF_HEADER = \"x-clipform-mcp-mint-proof\";\n\n/** #4090: carries `mcp_session_proof` (same trust model as MCP_MINT_PROOF_HEADER: HMAC-verified server-side, never trusted raw). */\nexport const MCP_SESSION_PROOF_HEADER = \"x-clipform-mcp-session-proof\";\n\n/** Carries `mcp_session_id` to the API for funnel analytics only (#4657). */\nexport const MCP_SESSION_ID_HEADER = \"x-clipform-mcp-session-id\";\n\nconst storage = new AsyncLocalStorage<McpAuthContext>();\n\nexport function runWithMcpAuth<T>(ctx: McpAuthContext, fn: () => Promise<T> | T): Promise<T> | T {\n return storage.run(ctx, fn);\n}\n\nexport function getMcpAuth(): McpAuthContext | undefined {\n return storage.getStore();\n}\n\nexport function runWithMcpTool<T>(toolName: string, fn: () => Promise<T> | T): Promise<T> | T {\n const parent = storage.getStore();\n if (!parent) return fn();\n return storage.run({ ...parent, tool_name: toolName }, fn);\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AA4F3B,IAAM,uBAAuB;AAa7B,IAAM,sBAAsB;AAW5B,IAAM,wBAAwB;AAG9B,IAAM,2BAA2B;AAGjC,IAAM,wBAAwB;AAErC,IAAM,UAAU,IAAI,kBAAkC;AAE/C,SAAS,eAAkB,KAAqB,IAA0C;AAC/F,SAAO,QAAQ,IAAI,KAAK,EAAE;AAC5B;AAEO,SAAS,aAAyC;AACvD,SAAO,QAAQ,SAAS;AAC1B;AAEO,SAAS,eAAkB,UAAkB,IAA0C;AAC5F,QAAM,SAAS,QAAQ,SAAS;AAChC,MAAI,CAAC,OAAQ,QAAO,GAAG;AACvB,SAAO,QAAQ,IAAI,EAAE,GAAG,QAAQ,WAAW,SAAS,GAAG,EAAE;AAC3D;","names":[]}
@@ -0,0 +1,102 @@
1
+ import {
2
+ CURRENT_PLAN_VERSIONS,
3
+ PLAN_LIMITS,
4
+ callApi
5
+ } from "./chunk-SHTP4AJC.js";
6
+ import {
7
+ getMcpAuth
8
+ } from "./chunk-VPF6MLKM.js";
9
+
10
+ // src/lib/caps.ts
11
+ var FULL_CAPS = Object.freeze({ media: true, research: true });
12
+ function capsFromToolsets(toolsets) {
13
+ if (toolsets === void 0) return FULL_CAPS;
14
+ return {
15
+ media: toolsets.includes("media"),
16
+ research: toolsets.includes("research")
17
+ };
18
+ }
19
+
20
+ // src/lib/anon-notice.ts
21
+ var ANON_PREVIEW_NOTICE = "Preview accounts are temporary - claim yours to keep your forms.";
22
+ function anonPreviewNotice() {
23
+ return getMcpAuth()?.is_anonymous ? ANON_PREVIEW_NOTICE : null;
24
+ }
25
+
26
+ // src/lib/session-context.ts
27
+ async function getSessionContextWithAuth() {
28
+ let me;
29
+ const authCtx = getMcpAuth();
30
+ if (authCtx?.loadInternalContent && !authCtx.api_key) {
31
+ const free = PLAN_LIMITS[CURRENT_PLAN_VERSIONS[0]];
32
+ me = {
33
+ auth_mode: "anonymous",
34
+ plan: {
35
+ name: free.name,
36
+ tier: free.tier,
37
+ node_limit: free.node_limit,
38
+ form_limit: free.form_limit ?? null,
39
+ custom_theme: free.custom_theme
40
+ }
41
+ };
42
+ } else {
43
+ let result;
44
+ try {
45
+ result = await callApi("/me", { method: "GET" });
46
+ } catch {
47
+ return { text: "", authMode: "anonymous" };
48
+ }
49
+ if (!result.ok) return { text: "", authMode: "anonymous" };
50
+ me = result.data;
51
+ }
52
+ const apiAuthMode = getMcpAuth()?.is_anonymous ? "anonymous" : me.auth_mode ?? "anonymous";
53
+ const authMode = apiAuthMode === "anonymous" ? "anonymous" : "authenticated";
54
+ const plan = me.plan;
55
+ const lines = [];
56
+ lines.push("## Your Session");
57
+ if (authMode === "authenticated") {
58
+ lines.push(
59
+ apiAuthMode === "api_key" ? "Auth: API key (workspace-scoped)" : "Auth: signed in (connected to a Clipform account)"
60
+ );
61
+ lines.push(`Workspace: ${me.workspace?.name ?? "Unknown"}`);
62
+ } else {
63
+ lines.push("Auth: anonymous (not connected to a Clipform account)");
64
+ if (me.workspace) {
65
+ lines.push(`Workspace: ${me.workspace.name}`);
66
+ }
67
+ }
68
+ lines.push(`Plan: ${plan.name} (tier ${plan.tier})`);
69
+ if (plan.node_limit !== null) {
70
+ lines.push(`Question limit: ${plan.node_limit} per form`);
71
+ } else {
72
+ lines.push("Questions: unlimited");
73
+ }
74
+ if (plan.form_limit !== null) {
75
+ lines.push(`Form limit: ${plan.form_limit} forms`);
76
+ } else {
77
+ lines.push("Forms: unlimited");
78
+ }
79
+ if (plan.custom_theme === false) {
80
+ lines.push("Custom themes: not available on this plan");
81
+ }
82
+ if (authMode === "anonymous") {
83
+ lines.push(`
84
+ Anonymous session: forms are not tied to a Clipform account until you sign in.`);
85
+ lines.push(ANON_PREVIEW_NOTICE);
86
+ }
87
+ return { text: lines.join("\n"), authMode };
88
+ }
89
+ async function getSessionContext() {
90
+ const { text } = await getSessionContextWithAuth();
91
+ return text;
92
+ }
93
+
94
+ export {
95
+ FULL_CAPS,
96
+ capsFromToolsets,
97
+ ANON_PREVIEW_NOTICE,
98
+ anonPreviewNotice,
99
+ getSessionContextWithAuth,
100
+ getSessionContext
101
+ };
102
+ //# sourceMappingURL=chunk-YOVLSXPW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/caps.ts","../src/lib/anon-notice.ts","../src/lib/session-context.ts"],"sourcesContent":["import type { McpToolset } from \"@vid-master/config\";\n\n/**\n * What a connected surface can actually do, derived from the connect-time\n * toolset filter (#3309) so the guidance layers (server instructions, prompt\n * recipes, `clipform_get_workflow`, craft guides) never reference a tool the\n * surface doesn't register (#3681).\n *\n * The Claude Connectors Directory surface (`mcp.clipform.io/forms`) is\n * account + forms only: no media generation and no research helpers. A\n * workflow that still says \"call clipform_generate_tts\" there sends the model\n * at a tool that doesn't exist, which shows up in review as failed tool\n * calls. Every guidance producer takes a ServerCaps and gates on it.\n */\nexport interface ServerCaps {\n /** Narration, video, image, music, render, and asset tools are registered. */\n media: boolean;\n /** News lookup and the generation audit log are registered. */\n research: boolean;\n}\n\n/** Every toolset registered - stdio/npx and the bare remote endpoint. */\nexport const FULL_CAPS: ServerCaps = Object.freeze({ media: true, research: true });\n\nexport function capsFromToolsets(toolsets?: readonly McpToolset[]): ServerCaps {\n if (toolsets === undefined) return FULL_CAPS;\n return {\n media: toolsets.includes(\"media\"),\n research: toolsets.includes(\"research\"),\n };\n}\n","import { getMcpAuth } from \"./auth-context.js\";\n\n/**\n * #4650: anonymous (preview) accounts are temporary. Date-free by design -\n * the purge is not enforced yet, so no expiry date or day count is promised\n * (exact dates are follow-up #4662). Defined once; every anonymous-lane\n * surface (create/update_form text, whoami, session resource) reuses it.\n */\nexport const ANON_PREVIEW_NOTICE =\n \"Preview accounts are temporary - claim yours to keep your forms.\";\n\n/** The notice for an anonymous MCP caller, or null for everyone else. */\nexport function anonPreviewNotice(): string | null {\n return getMcpAuth()?.is_anonymous ? ANON_PREVIEW_NOTICE : null;\n}\n","import { BUSINESS, PLAN_LIMITS, CURRENT_PLAN_VERSIONS } from \"@vid-master/config\";\nimport { callApi } from \"./api-client.js\";\nimport { ANON_PREVIEW_NOTICE } from \"./anon-notice.js\";\nimport { getMcpAuth } from \"./auth-context.js\";\n\nexport type AuthMode = \"authenticated\" | \"anonymous\";\n\nexport interface SessionContextResult {\n text: string;\n authMode: AuthMode;\n}\n\nexport async function getSessionContextWithAuth(): Promise<SessionContextResult> {\n let me: any;\n const authCtx = getMcpAuth();\n if (authCtx?.loadInternalContent && !authCtx.api_key) {\n // #4657: the no-workspace anon lane (get_workflow with no mint) has no\n // workspace for /me to describe, so plan caps default to Free.\n const free = PLAN_LIMITS[CURRENT_PLAN_VERSIONS[0]];\n me = {\n auth_mode: \"anonymous\",\n plan: {\n name: free.name,\n tier: free.tier,\n node_limit: free.node_limit,\n form_limit: free.form_limit ?? null,\n custom_theme: free.custom_theme,\n },\n };\n } else {\n let result;\n try {\n result = await callApi(\"/me\", { method: \"GET\" });\n } catch {\n return { text: \"\", authMode: \"anonymous\" };\n }\n if (!result.ok) return { text: \"\", authMode: \"anonymous\" };\n me = result.data as any;\n }\n // The API's auth_mode is \"api_key\" | \"oauth\" | \"session\" | \"anonymous\".\n // Anything credentialed counts as authenticated - the old `=== \"oauth\"`\n // check matched nothing the API returns, so API-key sessions were labelled\n // anonymous with a \"sign in to unlock\" nudge right next to their real\n // plan (#412 F6).\n // The anon MCP lane runs as a minted bot key (/me says \"api_key\"), so the\n // auth context is the truth for anonymous (#4653).\n const apiAuthMode: string = getMcpAuth()?.is_anonymous\n ? \"anonymous\"\n : ((me.auth_mode as string) ?? \"anonymous\");\n const authMode: AuthMode = apiAuthMode === \"anonymous\" ? \"anonymous\" : \"authenticated\";\n const plan = me.plan;\n const lines: string[] = [];\n\n lines.push(\"## Your Session\");\n if (authMode === \"authenticated\") {\n lines.push(\n apiAuthMode === \"api_key\"\n ? \"Auth: API key (workspace-scoped)\"\n : \"Auth: signed in (connected to a Clipform account)\"\n );\n // Workspace NAME only - the workspace UUID and the raw user_id/company_id\n // are internal database identifiers the model never needs (response\n // hygiene / disclosure guard, #2976).\n lines.push(`Workspace: ${me.workspace?.name ?? \"Unknown\"}`);\n } else {\n lines.push(\"Auth: anonymous (not connected to a Clipform account)\");\n if (me.workspace) {\n lines.push(`Workspace: ${me.workspace.name}`);\n }\n }\n lines.push(`Plan: ${plan.name} (tier ${plan.tier})`);\n if (plan.node_limit !== null) {\n lines.push(`Question limit: ${plan.node_limit} per form`);\n } else {\n lines.push(\"Questions: unlimited\");\n }\n if (plan.form_limit !== null) {\n lines.push(`Form limit: ${plan.form_limit} forms`);\n } else {\n lines.push(\"Forms: unlimited\");\n }\n if (plan.custom_theme === false) {\n lines.push(\"Custom themes: not available on this plan\");\n }\n if (authMode === \"anonymous\") {\n // Neutral statement of fact, no upsell/CTA copy (commerce sweep, #2976):\n // an anonymous session's forms are not tied to an account until one signs in.\n lines.push(`\\nAnonymous session: forms are not tied to a Clipform account until you sign in.`);\n lines.push(ANON_PREVIEW_NOTICE);\n }\n\n return { text: lines.join(\"\\n\"), authMode };\n}\n\nexport async function getSessionContext(): Promise<string> {\n const { text } = await getSessionContextWithAuth();\n return text;\n}\n"],"mappings":";;;;;;;;;;AAsBO,IAAM,YAAwB,OAAO,OAAO,EAAE,OAAO,MAAM,UAAU,KAAK,CAAC;AAE3E,SAAS,iBAAiB,UAA8C;AAC7E,MAAI,aAAa,OAAW,QAAO;AACnC,SAAO;AAAA,IACL,OAAO,SAAS,SAAS,OAAO;AAAA,IAChC,UAAU,SAAS,SAAS,UAAU;AAAA,EACxC;AACF;;;ACtBO,IAAM,sBACX;AAGK,SAAS,oBAAmC;AACjD,SAAO,WAAW,GAAG,eAAe,sBAAsB;AAC5D;;;ACFA,eAAsB,4BAA2D;AAC/E,MAAI;AACJ,QAAM,UAAU,WAAW;AAC3B,MAAI,SAAS,uBAAuB,CAAC,QAAQ,SAAS;AAGpD,UAAM,OAAO,YAAY,sBAAsB,CAAC,CAAC;AACjD,SAAK;AAAA,MACH,WAAW;AAAA,MACX,MAAM;AAAA,QACJ,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,YAAY,KAAK;AAAA,QACjB,YAAY,KAAK,cAAc;AAAA,QAC/B,cAAc,KAAK;AAAA,MACrB;AAAA,IACF;AAAA,EACF,OAAO;AACL,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,QAAQ,OAAO,EAAE,QAAQ,MAAM,CAAC;AAAA,IACjD,QAAQ;AACN,aAAO,EAAE,MAAM,IAAI,UAAU,YAAY;AAAA,IAC3C;AACA,QAAI,CAAC,OAAO,GAAI,QAAO,EAAE,MAAM,IAAI,UAAU,YAAY;AACzD,SAAK,OAAO;AAAA,EACd;AAQA,QAAM,cAAsB,WAAW,GAAG,eACtC,cACE,GAAG,aAAwB;AACjC,QAAM,WAAqB,gBAAgB,cAAc,cAAc;AACvE,QAAM,OAAO,GAAG;AAChB,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,iBAAiB;AAC5B,MAAI,aAAa,iBAAiB;AAChC,UAAM;AAAA,MACJ,gBAAgB,YACZ,qCACA;AAAA,IACN;AAIA,UAAM,KAAK,cAAc,GAAG,WAAW,QAAQ,SAAS,EAAE;AAAA,EAC5D,OAAO;AACL,UAAM,KAAK,uDAAuD;AAClE,QAAI,GAAG,WAAW;AAChB,YAAM,KAAK,cAAc,GAAG,UAAU,IAAI,EAAE;AAAA,IAC9C;AAAA,EACF;AACA,QAAM,KAAK,SAAS,KAAK,IAAI,UAAU,KAAK,IAAI,GAAG;AACnD,MAAI,KAAK,eAAe,MAAM;AAC5B,UAAM,KAAK,mBAAmB,KAAK,UAAU,WAAW;AAAA,EAC1D,OAAO;AACL,UAAM,KAAK,sBAAsB;AAAA,EACnC;AACA,MAAI,KAAK,eAAe,MAAM;AAC5B,UAAM,KAAK,eAAe,KAAK,UAAU,QAAQ;AAAA,EACnD,OAAO;AACL,UAAM,KAAK,kBAAkB;AAAA,EAC/B;AACA,MAAI,KAAK,iBAAiB,OAAO;AAC/B,UAAM,KAAK,2CAA2C;AAAA,EACxD;AACA,MAAI,aAAa,aAAa;AAG5B,UAAM,KAAK;AAAA,+EAAkF;AAC7F,UAAM,KAAK,mBAAmB;AAAA,EAChC;AAEA,SAAO,EAAE,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS;AAC5C;AAEA,eAAsB,oBAAqC;AACzD,QAAM,EAAE,KAAK,IAAI,MAAM,0BAA0B;AACjD,SAAO;AACT;","names":[]}
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import {
3
3
  JSONRPCMessageSchema,
4
4
  createServer
5
- } from "./chunk-4XCXDH3I.js";
6
- import "./chunk-22BV4NSB.js";
7
- import "./chunk-ELK26KWM.js";
8
- import "./chunk-SEZ37YSH.js";
9
- import "./chunk-A5CFGL27.js";
5
+ } from "./chunk-PXHGG57Z.js";
6
+ import "./chunk-GJ3XQSQY.js";
7
+ import "./chunk-5SCRFNYJ.js";
8
+ import "./chunk-YOVLSXPW.js";
9
+ import "./chunk-SHTP4AJC.js";
10
+ import "./chunk-VPF6MLKM.js";
10
11
  import {
11
12
  setMcpVersion
12
13
  } from "./chunk-PBQ5BQWD.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/src/server/stdio.ts","../../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/src/shared/stdio.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\nimport { setMcpVersion } from \"./lib/telemetry.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(__dirname, \"..\", \"package.json\"), \"utf-8\"));\nsetMcpVersion(pkg.version);\n\nconst telemetryOff =\n process.env.DO_NOT_TRACK === \"1\" || process.env.CLIPFORM_TELEMETRY === \"off\";\n\nconst server = createServer();\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\nconsole.error(`Clipform MCP Server v${pkg.version} running on stdio`);\nif (!telemetryOff) {\n console.error(\"Telemetry enabled (server errors only). Set DO_NOT_TRACK=1 to disable.\");\n}\n",null,null],"mappings":"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","names":["process","process"]}
1
+ {"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/src/server/stdio.ts","../../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/src/shared/stdio.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\nimport { setMcpVersion } from \"./lib/telemetry.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(__dirname, \"..\", \"package.json\"), \"utf-8\"));\nsetMcpVersion(pkg.version);\n\nconst telemetryOff =\n process.env.DO_NOT_TRACK === \"1\" || process.env.CLIPFORM_TELEMETRY === \"off\";\n\nconst server = createServer();\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\nconsole.error(`Clipform MCP Server v${pkg.version} running on stdio`);\nif (!telemetryOff) {\n console.error(\"Telemetry enabled (server errors only). Set DO_NOT_TRACK=1 to disable.\");\n}\n",null,null],"mappings":";;;;;;;;;;;;;;;;AACA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;;;ACH9B,OAAOA,cAAa;;;ACEb,IAAM,gCAAgC,KAAK,OAAO;AAKnD,IAAO,aAAP,MAAiB;EAInB,YAAY,SAAoC;AAC5C,SAAK,iBAAiB,SAAS,iBAAiB;EACpD;EAEA,OAAO,OAAa;AAChB,UAAM,WAAW,KAAK,SAAS,UAAU,KAAK,MAAM;AACpD,QAAI,UAAU,KAAK,gBAAgB;AAC/B,WAAK,MAAK;AACV,YAAM,IAAI,MAAM,uCAAuC,KAAK,cAAc,QAAQ;IACtF;AACA,SAAK,UAAU,KAAK,UAAU,OAAO,OAAO,CAAC,KAAK,SAAS,KAAK,CAAC,IAAI;EACzE;EAEA,cAAW;AACP,QAAI,CAAC,KAAK,SAAS;AACf,aAAO;IACX;AAEA,UAAM,QAAQ,KAAK,QAAQ,QAAQ,IAAI;AACvC,QAAI,UAAU,IAAI;AACd,aAAO;IACX;AAEA,UAAM,OAAO,KAAK,QAAQ,SAAS,QAAQ,GAAG,KAAK,EAAE,QAAQ,OAAO,EAAE;AACtE,SAAK,UAAU,KAAK,QAAQ,SAAS,QAAQ,CAAC;AAC9C,WAAO,mBAAmB,IAAI;EAClC;EAEA,QAAK;AACD,SAAK,UAAU;EACnB;;AAGE,SAAU,mBAAmB,MAAY;AAC3C,SAAO,qBAAqB,MAAM,KAAK,MAAM,IAAI,CAAC;AACtD;AAEM,SAAU,iBAAiB,SAAuB;AACpD,SAAO,KAAK,UAAU,OAAO,IAAI;AACrC;;;ADvCM,IAAO,uBAAP,MAA2B;EAI7B,YACY,SAAmBC,SAAQ,OAC3B,UAAoBA,SAAQ,QACpC,SAQC;AAVO,SAAA,SAAA;AACA,SAAA,UAAA;AAJJ,SAAA,WAAW;AAuBnB,SAAA,UAAU,CAAC,UAAiB;AACxB,UAAI;AACA,aAAK,YAAY,OAAO,KAAK;AAC7B,aAAK,kBAAiB;MAC1B,SAAS,OAAO;AACZ,aAAK,UAAU,KAAc;AAC7B,aAAK,MAAK,EAAG,MAAM,MAAK;QAAE,CAAC;MAC/B;IACJ;AACA,SAAA,WAAW,CAAC,UAAgB;AACxB,WAAK,UAAU,KAAK;IACxB;AAnBI,SAAK,cAAc,IAAI,WAAW,EAAE,eAAe,SAAS,cAAa,CAAE;EAC/E;;;;EAuBA,MAAM,QAAK;AACP,QAAI,KAAK,UAAU;AACf,YAAM,IAAI,MACN,+GAA+G;IAEvH;AAEA,SAAK,WAAW;AAChB,SAAK,OAAO,GAAG,QAAQ,KAAK,OAAO;AACnC,SAAK,OAAO,GAAG,SAAS,KAAK,QAAQ;EACzC;EAEQ,oBAAiB;AACrB,WAAO,MAAM;AACT,UAAI;AACA,cAAM,UAAU,KAAK,YAAY,YAAW;AAC5C,YAAI,YAAY,MAAM;AAClB;QACJ;AAEA,aAAK,YAAY,OAAO;MAC5B,SAAS,OAAO;AACZ,aAAK,UAAU,KAAc;MACjC;IACJ;EACJ;EAEA,MAAM,QAAK;AAEP,SAAK,OAAO,IAAI,QAAQ,KAAK,OAAO;AACpC,SAAK,OAAO,IAAI,SAAS,KAAK,QAAQ;AAGtC,UAAM,yBAAyB,KAAK,OAAO,cAAc,MAAM;AAC/D,QAAI,2BAA2B,GAAG;AAG9B,WAAK,OAAO,MAAK;IACrB;AAGA,SAAK,YAAY,MAAK;AACtB,SAAK,UAAS;EAClB;EAEA,KAAK,SAAuB;AACxB,WAAO,IAAI,QAAQ,aAAU;AACzB,YAAM,OAAO,iBAAiB,OAAO;AACrC,UAAI,KAAK,QAAQ,MAAM,IAAI,GAAG;AAC1B,gBAAO;MACX,OAAO;AACH,aAAK,QAAQ,KAAK,SAAS,OAAO;MACtC;IACJ,CAAC;EACL;;;;ADlGJ,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxD,IAAM,MAAM,KAAK,MAAM,aAAa,KAAK,WAAW,MAAM,cAAc,GAAG,OAAO,CAAC;AACnF,cAAc,IAAI,OAAO;AAEzB,IAAM,eACJ,QAAQ,IAAI,iBAAiB,OAAO,QAAQ,IAAI,uBAAuB;AAEzE,IAAM,SAAS,aAAa;AAC5B,IAAM,YAAY,IAAI,qBAAqB;AAC3C,MAAM,OAAO,QAAQ,SAAS;AAC9B,QAAQ,MAAM,wBAAwB,IAAI,OAAO,mBAAmB;AACpE,IAAI,CAAC,cAAc;AACjB,UAAQ,MAAM,wEAAwE;AACxF;","names":["process","process"]}
@@ -24,9 +24,26 @@ interface InstrumentServerOptions {
24
24
  */
25
25
  reportMissing?: boolean;
26
26
  }
27
+ /**
28
+ * #4623: per-request telemetry context stamped on every `$mcp_tool_call` /
29
+ * `$mcp_intent` in `beforeSend`. Analytics labels only - never an authority.
30
+ * Absent on the in-process lane, which then behaves exactly as before.
31
+ */
32
+ interface McpTelemetryContext {
33
+ /** Resolved client bucket (chatgpt/claude_connector/mcp_other/automated). Kept
34
+ * separate from the SDK's raw `$mcp_client_name`, which is NOT overwritten. */
35
+ clientOrigin?: string;
36
+ authMode?: "oauth" | "anon" | "api_key" | "none";
37
+ workspaceId?: string;
38
+ /** Anon lane: the minted/resolved anon uid, used as the distinctId so anon
39
+ * tool calls join the person instead of all landing on "anonymous". */
40
+ anonUid?: string;
41
+ /** ChatGPT `_meta["openai/locale"]` (BCP 47). Language hint only - never location. */
42
+ clientLocale?: string;
43
+ }
27
44
  declare function buildInstrumentOptions(mcpAuth: {
28
45
  user_id?: string;
29
- } | null, opts?: InstrumentServerOptions): MCPAnalyticsOptions;
46
+ } | null, opts?: InstrumentServerOptions, telemetry?: McpTelemetryContext): MCPAnalyticsOptions;
30
47
  /**
31
48
  * Wraps the per-request/per-connection server with PostHog MCP analytics
32
49
  * (#3408, extended in-process #3432), additively - `mcp_request`/`$exception`
@@ -37,6 +54,6 @@ declare function buildInstrumentOptions(mcpAuth: {
37
54
  */
38
55
  declare function instrumentServer(server: unknown, posthogClient: McpAnalyticsClient | null, mcpAuth: {
39
56
  user_id?: string;
40
- } | null, opts?: InstrumentServerOptions): void;
57
+ } | null, opts?: InstrumentServerOptions, telemetry?: McpTelemetryContext): void;
41
58
 
42
- export { type InstrumentServerOptions, buildInstrumentOptions, getMcpAnalyticsClient, instrumentServer, setMcpAnalyticsClient };
59
+ export { type InstrumentServerOptions, type McpTelemetryContext, buildInstrumentOptions, getMcpAnalyticsClient, instrumentServer, setMcpAnalyticsClient };
@@ -5236,7 +5236,7 @@ function setMcpAnalyticsClient(client) {
5236
5236
  function getMcpAnalyticsClient() {
5237
5237
  return installedClient;
5238
5238
  }
5239
- function buildInstrumentOptions(mcpAuth, opts = {}) {
5239
+ function buildInstrumentOptions(mcpAuth, opts = {}, telemetry) {
5240
5240
  return {
5241
5241
  context: {
5242
5242
  description: "Describe the user's underlying goal in one sentence - not the tool you're calling."
@@ -5245,13 +5245,19 @@ function buildInstrumentOptions(mcpAuth, opts = {}) {
5245
5245
  // Static identity: a fresh server + instrument() call happens once per
5246
5246
  // stateless request, so mcpAuth is already resolved by call time. Matches
5247
5247
  // the mcp_request/$exception distinctId below so all three lines join.
5248
- identify: { distinctId: mcpAuth?.user_id || "anonymous" },
5248
+ identify: { distinctId: mcpAuth?.user_id || telemetry?.anonUid || "anonymous" },
5249
5249
  // PRIVACY PASS (non-negotiable, #797/#3200): tool params/responses carry
5250
5250
  // form content, share ids, and workspace data - ship intent + tool name +
5251
5251
  // timing + error flag ONLY, never the payloads.
5252
5252
  beforeSend: (event) => {
5253
5253
  delete event.properties[PostHogMCPAnalyticsProperty.Parameters];
5254
5254
  delete event.properties[PostHogMCPAnalyticsProperty.Response];
5255
+ if (telemetry) {
5256
+ if (telemetry.clientOrigin) event.properties.client_origin = telemetry.clientOrigin;
5257
+ if (telemetry.authMode) event.properties.auth_mode = telemetry.authMode;
5258
+ if (telemetry.workspaceId) event.properties.workspace_id = telemetry.workspaceId;
5259
+ if (telemetry.clientLocale) event.properties.client_locale = telemetry.clientLocale;
5260
+ }
5255
5261
  return event;
5256
5262
  },
5257
5263
  // We already emit `$exception` with `$exception_source: "mcp-server"` via
@@ -5266,10 +5272,10 @@ function buildInstrumentOptions(mcpAuth, opts = {}) {
5266
5272
  reportMissing: opts.reportMissing ?? true
5267
5273
  };
5268
5274
  }
5269
- function instrumentServer(server, posthogClient, mcpAuth, opts = {}) {
5275
+ function instrumentServer(server, posthogClient, mcpAuth, opts = {}, telemetry) {
5270
5276
  if (!posthogClient) return;
5271
5277
  try {
5272
- instrument(server, posthogClient, buildInstrumentOptions(mcpAuth, opts));
5278
+ instrument(server, posthogClient, buildInstrumentOptions(mcpAuth, opts, telemetry));
5273
5279
  } catch (err) {
5274
5280
  console.error("[mcp] Failed to instrument server for PostHog MCP analytics:", err);
5275
5281
  }