@pitcher/cli-scripts 3.0.1 → 3.1.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.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Releases are published to npm automatically: a merge to `main` that bumps `versi
6
6
 
7
7
  > ⚠️ **Breaking changes in 3.0.0**
8
8
  >
9
- > 1. **Local `p mcp` now serves ONLY 3 tools** (`pitcher_dev_sync`, `pitcher_dev_sync_revert`, `pitcher_publish_app`), down from 124. Everything else is the hosted server `https://pitcher-mcp.fly.dev/mcp`. `p mcp` no longer reads `NG_API_KEY`/`NG_ORG`/`NG_URL`/`NG_ENV`; run `p login` once (`--domain` or `PITCHER_DOMAIN` when logged in to several orgs).
9
+ > 1. **Local `p mcp` now serves ONLY 3 tools** (`pitcher_dev_sync`, `pitcher_dev_sync_revert`, `pitcher_publish_app`), down from 124. Everything else is the hosted server `https://mcp.pitcher.com/mcp`. `p mcp` no longer reads `NG_API_KEY`/`NG_ORG`/`NG_URL`/`NG_ENV`; run `p login` once (`--domain` or `PITCHER_DOMAIN` when logged in to several orgs).
10
10
  > 2. **Every other command uses your `p login` session.** `--api-key` (automation) still wins when passed; `NG_API_KEY` is used only when you have NO `p login` session, and is otherwise ignored with a notice. `--org` no longer defaults to `dev` on the session path: the session's org is the target. `--domain` cannot be combined with `--api-key` or `--org`.
11
11
  > 3. **The Claude Desktop `.mcpb` extension is retired.** Claude Desktop adds the remote URL natively.
12
12
  > 4. **`p analytics kpi` requires `--type`** (one of the 12 KPI types listed in `p analytics kpi --help`). The old default `all` always returned a 500.
@@ -44,7 +44,7 @@ Pitcher's MCP tools come from two places:
44
44
 
45
45
  | Server | Tools | Add it with |
46
46
  |---|---|---|
47
- | **Hosted** (OAuth; Claude Code, Claude Desktop, claude.ai, ChatGPT) | everything: content, canvases, DSRs, analytics, L&D, users, … (121 tools, prompts, resources) | `claude mcp add --transport http pitcher https://pitcher-mcp.fly.dev/mcp` — Claude Desktop: Settings → Connectors → Add custom connector, same URL |
47
+ | **Hosted** (OAuth; Claude Code, Claude Desktop, claude.ai, ChatGPT) | everything: content, canvases, DSRs, analytics, L&D, users, … (121 tools, prompts, resources) | `claude mcp add --transport http pitcher https://mcp.pitcher.com/mcp` — Claude Desktop: Settings → Connectors → Add custom connector, same URL |
48
48
  | **Local** `p mcp` (stdio) | only `pitcher_dev_sync`, `pitcher_dev_sync_revert`, `pitcher_publish_app` (they zip a folder on YOUR machine) | `claude mcp add pitcher-local -- p mcp` |
49
49
 
50
50
  Claude Code users add both. The local server runs on your `p login` session (no API key, no env block); the domain is fixed when it starts. Signed in to several orgs? Add one entry per org:
@@ -502,7 +502,7 @@ p metadata-template scaffold --out ./schema.json
502
502
  p metadata-template upsert --instance-id 01... --sections-file ./schema.json --api-key myapikey
503
503
  ```
504
504
 
505
- `upsert` merges the incoming sections by section `label` — a section whose label matches an existing one is replaced, other sections are preserved. It **cannot remove** a section or field (that's a UI operation). `options` for `select`/`multi-select` fields are `{ label, value }` objects (bare strings are rejected). `upsert` requires an instance- or org-admin key; a non-admin member key is rejected (400 → `VALIDATION`).
505
+ `upsert` merges the incoming sections by section `label` — a section whose label matches an existing one is **replaced whole** (fields left out of it are removed), other sections are preserved. It cannot remove a whole section (that's a UI operation). `options` for `select`/`multi-select` fields are `{ label, value }` objects (bare strings are rejected). `upsert` requires an instance- or org-admin key; a non-admin member key is rejected (400 → `VALIDATION`).
506
506
 
507
507
  ## To run the `dsr` command:
508
508
 
@@ -69,8 +69,9 @@ export async function runMetadataTemplateUpsert(opts, deps = {}) {
69
69
  const UPSERT_HELP = `
70
70
  Upserts the instance's metadata-field schema. POST is an upsert keyed by
71
71
  instance that MERGES the incoming sections by section 'label' — a section whose
72
- label matches an existing one is replaced, other sections are preserved. It
73
- CANNOT remove a section or field (a full-replace / UI operation).
72
+ label matches an existing one is replaced whole (fields left out of it are
73
+ removed), other sections are preserved. It cannot remove a whole section (a
74
+ full-replace / UI operation).
74
75
  --instance-id the instance whose schema to upsert (required)
75
76
  --sections inline JSON array of sections, OR
76
77
  --sections-file path to a JSON file with the sections array (exactly one)
@@ -205,7 +205,6 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
205
205
  is_required: true,
206
206
  is_filterable: true,
207
207
  is_sortable: true,
208
- is_pitch_master: false,
209
208
  help_text: 'Marketed product name',
210
209
  default: '',
211
210
  },
@@ -216,7 +215,6 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
216
215
  is_required: false,
217
216
  is_filterable: true,
218
217
  is_sortable: true,
219
- is_pitch_master: false,
220
218
  },
221
219
  {
222
220
  name: 'therapeutic_area',
@@ -225,7 +223,6 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
225
223
  is_required: false,
226
224
  is_filterable: true,
227
225
  is_sortable: false,
228
- is_pitch_master: false,
229
226
  options: [
230
227
  { label: 'Cardiology', value: 'cardiology' },
231
228
  { label: 'Oncology', value: 'oncology' },
@@ -239,7 +236,6 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
239
236
  is_required: false,
240
237
  is_filterable: true,
241
238
  is_sortable: false,
242
- is_pitch_master: false,
243
239
  options: [
244
240
  { label: 'HCP', value: 'hcp' },
245
241
  { label: 'Payer', value: 'payer' },
@@ -253,7 +249,6 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
253
249
  is_required: false,
254
250
  is_filterable: false,
255
251
  is_sortable: true,
256
- is_pitch_master: false,
257
252
  default: 0,
258
253
  },
259
254
  {
@@ -263,7 +258,6 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
263
258
  is_required: false,
264
259
  is_filterable: true,
265
260
  is_sortable: false,
266
- is_pitch_master: false,
267
261
  default: false,
268
262
  },
269
263
  ],
@@ -65,7 +65,11 @@ export declare function buildAuthorizeUrl(p: {
65
65
  state: string;
66
66
  codeChallenge: string;
67
67
  }): string;
68
- /** null = the domain is not a Pitcher org (4xx / no auth0 block). Throws an UpstreamError on network/5xx. */
68
+ /**
69
+ * null = the domain is not a Pitcher org (404, or no auth0 block). Throws an UpstreamError on a network error or
70
+ * any other status: a 400 or a 429 says nothing about the org, and the hosted server reads null as "this token's
71
+ * org is gone" (401, sign in again) where an upstream error is a retryable 503.
72
+ */
69
73
  export declare function fetchOrgConfig(domain: string, opts?: HttpOpts): Promise<OrgConfig | null>;
70
74
  export declare function exchangeCode(p: {
71
75
  auth0Domain: string;
@@ -35,6 +35,8 @@ export function orgSubdomain(domain) {
35
35
  export const AUTH0_NATIVE_CLIENT_IDS = {
36
36
  'auth0.test-eu.pitcher.com': 'EvXiukSFA9LmxkoMyGfGrX6tk5eZdJFu',
37
37
  'auth0.eu.pitcher.com': 'cJpAajaha7dWc2mnGy3GZSMBm3kzX370',
38
+ 'auth0.test-us.pitcher.com': 'sd6biLE9qGBwUfSzl2uRC1p5JdhKY06N',
39
+ 'auth0.us.pitcher.com': 'k6BgMI2Oy26RnyucUsWiL81JOeaufECm',
38
40
  };
39
41
  export function createUpstreamError(kind, message) {
40
42
  return Object.assign(new Error(message), { isUpstreamError: true, kind });
@@ -60,10 +62,16 @@ export function buildAuthorizeUrl(p) {
60
62
  }).toString();
61
63
  return u.href;
62
64
  }
63
- /** null = the domain is not a Pitcher org (4xx / no auth0 block). Throws an UpstreamError on network/5xx. */
65
+ /**
66
+ * null = the domain is not a Pitcher org (404, or no auth0 block). Throws an UpstreamError on a network error or
67
+ * any other status: a 400 or a 429 says nothing about the org, and the hosted server reads null as "this token's
68
+ * org is gone" (401, sign in again) where an upstream error is a retryable 503.
69
+ */
64
70
  export async function fetchOrgConfig(domain, opts = {}) {
65
71
  const doFetch = opts.fetch ?? globalThis.fetch;
66
- const url = `https://${domain}/core/api/org-config?org_subdomain=${encodeURIComponent(orgSubdomain(domain))}`;
72
+ // No `org_subdomain` param: the edge sets `x-original-domain` from the host, which names the same org, and
73
+ // next-core reports every request carrying the param to Sentry as deprecated.
74
+ const url = `https://${domain}/core/api/org-config`;
67
75
  let res;
68
76
  try {
69
77
  res = await doFetch(url, {
@@ -76,10 +84,10 @@ export async function fetchOrgConfig(domain, opts = {}) {
76
84
  catch {
77
85
  throw createUpstreamError('upstream', 'org-config unreachable');
78
86
  }
79
- if (res.status >= 500)
80
- throw createUpstreamError('upstream', `org-config returned ${res.status}`);
81
- if (res.status !== 200)
87
+ if (res.status === 404)
82
88
  return null;
89
+ if (res.status !== 200)
90
+ throw createUpstreamError('upstream', `org-config returned ${res.status}`);
83
91
  const json = await res.json().catch(() => null);
84
92
  const a = json?.auth0;
85
93
  if (!a || typeof a.domain !== 'string' || typeof a.audience !== 'string')
@@ -1,3 +1,4 @@
1
+ import type { PromptDescriptor } from './prompts/types.js';
1
2
  import type { BuildServerOptions } from './server.js';
2
3
  import type { ToolDescriptor } from './tools/types.js';
3
4
  /**
@@ -16,6 +17,20 @@ export declare const LOCAL_ONLY: ReadonlySet<string>;
16
17
  * exposes today (`questionFile`, `answersFile`, `canvasFile`, `assessmentsFile`, `contentHtmlFile`).
17
18
  */
18
19
  export declare const FILE_KEY: RegExp;
20
+ /**
21
+ * An id-shaped input (`id`, `instanceId`, `courseIds`, …). Many commands interpolate ids straight into the
22
+ * request PATH (`/api/v1/files/${id}`, the L&D `/admin/courses/${id}`), so a value like `../users/1` or `1?x=`
23
+ * would retarget the call to another endpoint on the same host with the caller's token. Hosted accepts only
24
+ * plain tokens (letters, digits, `_`, `-`; comma-separated lists for string list inputs).
25
+ */
26
+ export declare const ID_KEY: RegExp;
19
27
  /** Build options every hosted server uses: a write always asks the client to confirm. */
20
28
  export declare const HOSTED_BUILD_OPTIONS: Readonly<BuildServerOptions>;
21
29
  export declare const hostedTools: ToolDescriptor[];
30
+ /**
31
+ * Read-only mode (the org's `settings.mcp.access` is `read_only`, PIT-9768): only the hosted tools that read.
32
+ * A write tool is never registered, so calling one is "unknown tool", not a refused write.
33
+ */
34
+ export declare const hostedReadOnlyTools: ToolDescriptor[];
35
+ /** The prompts a read-only server serves: only those flagged `readOnly` (the rest drive a write tool). */
36
+ export declare const readOnlyPrompts: PromptDescriptor[];
@@ -1,5 +1,6 @@
1
1
  import { fail } from '../core/envelope.js';
2
2
  import { createCliError } from '../core/errors.js';
3
+ import { allPrompts } from './prompts/registry.js';
3
4
  import { allTools } from './registry.js';
4
5
  /**
5
6
  * The tool set served by the hosted (remote, OAuth) MCP server — PIT-8912.
@@ -21,6 +22,26 @@ export const LOCAL_ONLY = new Set([
21
22
  * exposes today (`questionFile`, `answersFile`, `canvasFile`, `assessmentsFile`, `contentHtmlFile`).
22
23
  */
23
24
  export const FILE_KEY = /File$|^filePath$/;
25
+ /**
26
+ * An id-shaped input (`id`, `instanceId`, `courseIds`, …). Many commands interpolate ids straight into the
27
+ * request PATH (`/api/v1/files/${id}`, the L&D `/admin/courses/${id}`), so a value like `../users/1` or `1?x=`
28
+ * would retarget the call to another endpoint on the same host with the caller's token. Hosted accepts only
29
+ * plain tokens (letters, digits, `_`, `-`; comma-separated lists for string list inputs).
30
+ */
31
+ export const ID_KEY = /^(id|ids|.+Id|.+Ids)$/;
32
+ const ID_TOKEN = /^[A-Za-z0-9_-]{1,200}$/;
33
+ function isSafeIdValue(v) {
34
+ // '' is "absent" to every command (falsy) and reaches no path
35
+ if (v === undefined || v === null || v === '' || typeof v === 'boolean')
36
+ return true;
37
+ if (typeof v === 'number')
38
+ return Number.isSafeInteger(v);
39
+ if (typeof v === 'string')
40
+ return v.split(',').every((part) => ID_TOKEN.test(part.trim()));
41
+ if (Array.isArray(v))
42
+ return v.every((x) => (typeof x === 'number' ? Number.isSafeInteger(x) : ID_TOKEN.test(String(x))));
43
+ return false;
44
+ }
24
45
  /** Build options every hosted server uses: a write always asks the client to confirm. */
25
46
  export const HOSTED_BUILD_OPTIONS = Object.freeze({ destructiveWrites: true });
26
47
  // Tool descriptions rewritten without the local-file wording (the stdio text is kept for p mcp).
@@ -29,7 +50,7 @@ const HOSTED_DESCRIPTIONS = {
29
50
  pitcher_files_update: 'Update a file by ID (name, folder, access, tags, metadata). sourceUrl replaces/overwrites the file binary; metadata is deep-merged.',
30
51
  pitcher_announcements_create: 'Create a wa-announcements appsDB record (title/body, optional media, CTA link, scheduling, group targeting). Requires title and body. Additive.',
31
52
  pitcher_dsr_create: 'Create a Digital Sales Room (a canvas share link) and get a clickable link back. Source is a canvas (canvasId) OR files (shareboxFileIds) - exactly one. Optional email/domain gating and a bound landing-page template. Returns the share: id, url (the full link), short_url, and a nested canvas object - there is no full_url field. This endpoint is a GET-OR-CREATE: sharing an already-shared canvas returns the existing link and only updates the fields you pass, so use pitcher_dsr_update to change or remove a value on an existing share. A failed landing bind does not fail the call: it returns the link with a warning - and binding a landing page needs instanceId, without which the bind is skipped with that warning. tags are a ShareBox-only field (they name the canvas the server builds from shareboxFileIds), so non-empty tags together with canvasId are REJECTED rather than silently dropped by the server.',
32
- pitcher_metadata_template_upsert: "Upsert an instance's metadata-field schema. Merges the incoming sections by section 'label' — a section whose label matches an existing one is replaced, other sections are preserved. Cannot remove a section or field. Requires an instance- or org-admin role.",
53
+ pitcher_metadata_template_upsert: "Upsert an instance's metadata-field schema. Merges the incoming sections by section 'label' — a section whose label matches an existing one is REPLACED WHOLE (fields left out of it are removed), other sections are preserved. Cannot remove a whole section. To add a field, send the section with all its existing fields plus the new one. Requires an instance- or org-admin role.",
33
54
  };
34
55
  // Field descriptions that name a stripped key, rewritten for hosted (`tool name → field → text`).
35
56
  const HOSTED_FIELD_DESCRIPTIONS = {
@@ -67,16 +88,28 @@ function toHosted(t) {
67
88
  ...t,
68
89
  description: HOSTED_DESCRIPTIONS[t.name] ?? t.description,
69
90
  inputShape: hostedShape(t),
70
- // Runtime guard, independent of the schema: the stripped keys are invisible to a well-behaved
91
+ // Runtime guards, independent of the schema: the stripped keys are invisible to a well-behaved
71
92
  // client, but this refuses any file-path key however it arrives (a schema that grows a new
72
- // xFile key, a caller bypassing validation, a command-layer key no schema declares).
93
+ // xFile key, a caller bypassing validation, a command-layer key no schema declares), and any
94
+ // id that is not a plain token (ID_KEY).
73
95
  run: (opts, deps) => {
74
96
  const bad = Object.keys(opts).filter((k) => FILE_KEY.test(k) && opts[k] !== undefined);
75
97
  if (bad.length) {
76
98
  return Promise.resolve(fail(createCliError('VALIDATION', `not supported on the hosted server (no local files): ${bad.join(', ')}`)));
77
99
  }
100
+ const badIds = Object.keys(opts).filter((k) => ID_KEY.test(k) && !isSafeIdValue(opts[k]));
101
+ if (badIds.length) {
102
+ return Promise.resolve(fail(createCliError('VALIDATION', `not a valid id (letters, digits, "_" and "-" only): ${badIds.join(', ')}`)));
103
+ }
78
104
  return t.run(opts, deps);
79
105
  },
80
106
  };
81
107
  }
82
108
  export const hostedTools = allTools.filter((t) => !LOCAL_ONLY.has(t.name)).map(toHosted);
109
+ /**
110
+ * Read-only mode (the org's `settings.mcp.access` is `read_only`, PIT-9768): only the hosted tools that read.
111
+ * A write tool is never registered, so calling one is "unknown tool", not a refused write.
112
+ */
113
+ export const hostedReadOnlyTools = hostedTools.filter((t) => t.readOnly && !t.write && !t.alwaysDestructive);
114
+ /** The prompts a read-only server serves: only those flagged `readOnly` (the rest drive a write tool). */
115
+ export const readOnlyPrompts = allPrompts.filter((p) => p.readOnly === true);
@@ -1,5 +1,5 @@
1
- export { buildServer, type McpRunContext, type BuildServerOptions } from './server.js';
2
- export { hostedTools, LOCAL_ONLY, FILE_KEY, HOSTED_BUILD_OPTIONS } from './hosted.js';
1
+ export { buildServer, PITCHER_ICON, type McpRunContext, type BuildServerOptions } from './server.js';
2
+ export { hostedTools, hostedReadOnlyTools, readOnlyPrompts, LOCAL_ONLY, FILE_KEY, ID_KEY, HOSTED_BUILD_OPTIONS, } from './hosted.js';
3
3
  export { allTools } from './registry.js';
4
4
  export { allPrompts } from './prompts/registry.js';
5
5
  export { allResources } from './resources/registry.js';
@@ -1,6 +1,6 @@
1
1
  // Public entry `@pitcher/cli-scripts/mcp` — what the hosted MCP server (web/packages/mcp-server) consumes.
2
- export { buildServer } from './server.js';
3
- export { hostedTools, LOCAL_ONLY, FILE_KEY, HOSTED_BUILD_OPTIONS } from './hosted.js';
2
+ export { buildServer, PITCHER_ICON } from './server.js';
3
+ export { hostedTools, hostedReadOnlyTools, readOnlyPrompts, LOCAL_ONLY, FILE_KEY, ID_KEY, HOSTED_BUILD_OPTIONS, } from './hosted.js';
4
4
  export { allTools } from './registry.js';
5
5
  export { allPrompts } from './prompts/registry.js';
6
6
  export { allResources } from './resources/registry.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The tool set of LOCAL `p mcp` (PIT-8943): only the three disk-bound tools (`dev_sync`, `dev_sync_revert`,
3
3
  * `publish_app`), which zip a folder on this machine and so cannot run hosted. Everything else is the hosted
4
- * server (`https://pitcher-mcp.fly.dev/mcp`).
4
+ * server (`https://mcp.pitcher.com/mcp`).
5
5
  */
6
6
  export declare const localTools: import("./index.js").ToolDescriptor[];
@@ -3,6 +3,6 @@ import { allTools } from './registry.js';
3
3
  /**
4
4
  * The tool set of LOCAL `p mcp` (PIT-8943): only the three disk-bound tools (`dev_sync`, `dev_sync_revert`,
5
5
  * `publish_app`), which zip a folder on this machine and so cannot run hosted. Everything else is the hosted
6
- * server (`https://pitcher-mcp.fly.dev/mcp`).
6
+ * server (`https://mcp.pitcher.com/mcp`).
7
7
  */
8
8
  export const localTools = allTools.filter((t) => LOCAL_ONLY.has(t.name));
@@ -7,6 +7,7 @@ import { instanceLine, readSafetyNote } from './shared.js';
7
7
  export const analyticsPrompts = [
8
8
  {
9
9
  name: 'top_content',
10
+ readOnly: true,
10
11
  title: 'Top-performing content',
11
12
  description: 'Rank an instance’s most-viewed canvases (and optionally files) over a window.',
12
13
  argsSchema: {
@@ -34,6 +35,7 @@ export const analyticsPrompts = [
34
35
  },
35
36
  {
36
37
  name: 'content_engagement_summary',
38
+ readOnly: true,
37
39
  title: 'Content engagement summary',
38
40
  description: 'Summarize per-content/DSR engagement for an instance over a window.',
39
41
  argsSchema: {
@@ -59,6 +61,7 @@ export const analyticsPrompts = [
59
61
  },
60
62
  {
61
63
  name: 'who_viewed_canvas',
64
+ readOnly: true,
62
65
  title: 'Who viewed a DSR canvas',
63
66
  description: 'List the viewers of a specific DSR canvas, then drill into one viewer’s session timeline.',
64
67
  argsSchema: {
@@ -89,6 +92,7 @@ export const analyticsPrompts = [
89
92
  },
90
93
  {
91
94
  name: 'adoption_snapshot',
95
+ readOnly: true,
92
96
  title: 'Adoption snapshot',
93
97
  description: 'Combine active-user stats and KPIs into a short adoption snapshot for an instance.',
94
98
  argsSchema: {
@@ -6,4 +6,5 @@ export interface PromptDescriptor {
6
6
  description: string;
7
7
  argsSchema: Record<string, ZodOptional<ZodString>>;
8
8
  build: (args: Record<string, string | undefined>) => PromptMessage[];
9
+ readOnly?: true;
9
10
  }
@@ -3,6 +3,7 @@ import type { Credential } from '../core/credential.js';
3
3
  import type { ToolDescriptor } from './tools/types.js';
4
4
  import type { PromptDescriptor } from './prompts/types.js';
5
5
  import type { ResourceDescriptor } from './resources/types.js';
6
+ export declare const PITCHER_ICON = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiI+PHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSIxMTYiIGZpbGw9IiMyOTMzNUMiLz48Y2lyY2xlIGN4PSIzNzIiIGN5PSIxNTAiIHI9IjMwIiBmaWxsPSIjQ0MwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTE1MCAxMTZoOTZhOTIgOTIgMCAwIDEgMCAxODRoLTUwdjk2aC00NnoiLz48cGF0aCBmaWxsPSIjMjkzMzVDIiBkPSJNMTk2IDE2MHY5Nmg1MGE0OCA0OCAwIDAgMCAwLTk2eiIvPjwvc3ZnPg==";
6
7
  /**
7
8
  * The per-server request context: who every tool call runs as, and against which origin.
8
9
  * Captured at build time — local stdio `p mcp` builds one from the `p login` session; the hosted server
@@ -18,10 +18,10 @@ const version = (() => {
18
18
  return '0.0.0';
19
19
  }
20
20
  })();
21
- // Connector branding shown by MCP hosts (Claude Desktop, Cursor). Self-contained
22
- // data URI so it needs no network/asset resolution: a navy (#29335C) tile with a
21
+ // Connector branding shown by MCP hosts (Claude Desktop, Cursor) and on the hosted server's own pages.
22
+ // Self-contained data URI so it needs no network/asset resolution: a navy (#29335C) tile with a
23
23
  // white "P" and the Pitcher-red (#CC0000) accent from the wordmark.
24
- const icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiI+PHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSIxMTYiIGZpbGw9IiMyOTMzNUMiLz48Y2lyY2xlIGN4PSIzNzIiIGN5PSIxNTAiIHI9IjMwIiBmaWxsPSIjQ0MwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTE1MCAxMTZoOTZhOTIgOTIgMCAwIDEgMCAxODRoLTUwdjk2aC00NnoiLz48cGF0aCBmaWxsPSIjMjkzMzVDIiBkPSJNMTk2IDE2MHY5Nmg1MGE0OCA0OCAwIDAgMCAwLTk2eiIvPjwvc3ZnPg==';
24
+ export const PITCHER_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiI+PHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSIxMTYiIGZpbGw9IiMyOTMzNUMiLz48Y2lyY2xlIGN4PSIzNzIiIGN5PSIxNTAiIHI9IjMwIiBmaWxsPSIjQ0MwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTE1MCAxMTZoOTZhOTIgOTIgMCAwIDEgMCAxODRoLTUwdjk2aC00NnoiLz48cGF0aCBmaWxsPSIjMjkzMzVDIiBkPSJNMTk2IDE2MHY5Nmg1MGE0OCA0OCAwIDAgMCAwLTk2eiIvPjwvc3ZnPg==';
25
25
  // Compact, sanitized error string (no raw stack) — shared by the prompt/resource callbacks.
26
26
  function errorText(e) {
27
27
  if (isCliError(e))
@@ -35,7 +35,7 @@ export function buildServer(ctx, tools = allTools, prompts = allPrompts, resourc
35
35
  title: 'Pitcher',
36
36
  description: 'Search, manage, and share Pitcher content, canvases, files, DSRs, and instances.',
37
37
  websiteUrl: 'https://www.pitcher.com',
38
- icons: [{ src: icon, mimeType: 'image/svg+xml', sizes: ['any'] }],
38
+ icons: [{ src: PITCHER_ICON, mimeType: 'image/svg+xml', sizes: ['any'] }],
39
39
  },
40
40
  // Surfaced to the host model via InitializeResult.instructions — the analytics answer-rendering
41
41
  // policy so a natural-language question gets a right-sized reply (sentence / table / chart /
@@ -25,8 +25,9 @@ export const metadataTemplateTools = [
25
25
  name: 'pitcher_metadata_template_upsert',
26
26
  title: 'Upsert a metadata template',
27
27
  description: "Upsert an instance's metadata-field schema. Merges the incoming sections by section 'label' — a " +
28
- 'section whose label matches an existing one is replaced, other sections are preserved. Cannot remove ' +
29
- 'a section or field. Pass exactly one of sections or sectionsFile. Requires an instance- or org-admin key.',
28
+ 'section whose label matches an existing one is REPLACED WHOLE (fields left out of it are removed), other ' +
29
+ 'sections are preserved. Cannot remove a whole section. Pass exactly one of sections or sectionsFile. ' +
30
+ 'Requires an instance- or org-admin key.',
30
31
  inputShape: {
31
32
  instanceId: z.string().describe('Instance ID (required)'),
32
33
  sections: z.string().optional().describe('Sections JSON array (inline)'),
@@ -6,7 +6,7 @@ export interface ToolDescriptor {
6
6
  name: string;
7
7
  title: string;
8
8
  description: string;
9
- inputShape: z.ZodRawShape;
9
+ inputShape: Record<string, z.ZodType>;
10
10
  run: (opts: Record<string, unknown> & InjectedAuth, deps?: any) => Promise<Envelope<unknown>>;
11
11
  wraps?: (...a: any[]) => unknown;
12
12
  readOnly: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitcher/cli-scripts",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "description": "The Pitcher CLI for various tasks in its ecosystem.",
5
5
  "author": "Pitcher AG",
6
6
  "license": "MIT",
@@ -57,6 +57,6 @@
57
57
  "chokidar": "^5.0.0",
58
58
  "commander": "^15.0.0",
59
59
  "form-data": "^4.0.6",
60
- "zod": "^3"
60
+ "zod": "^4.6.7"
61
61
  }
62
62
  }