@pitcher/cli-scripts 2.8.0 → 2.10.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.
Files changed (86) hide show
  1. package/README.md +56 -1
  2. package/dist/index.js +2 -0
  3. package/dist/src/commands/canvases.js +11 -8
  4. package/dist/src/commands/dsrLanding.d.ts +2 -0
  5. package/dist/src/commands/dsrLanding.js +18 -10
  6. package/dist/src/commands/emailTemplate.d.ts +1 -0
  7. package/dist/src/commands/emailTemplate.js +8 -4
  8. package/dist/src/commands/mcp.d.ts +2 -0
  9. package/dist/src/commands/mcp.js +18 -0
  10. package/dist/src/core/readInput.d.ts +2 -0
  11. package/dist/src/core/readInput.js +24 -0
  12. package/dist/src/mcp/bundled.d.ts +4 -0
  13. package/dist/src/mcp/bundled.js +25 -0
  14. package/dist/src/mcp/env.d.ts +7 -0
  15. package/dist/src/mcp/env.js +13 -0
  16. package/dist/src/mcp/prompts/dsrLanding.d.ts +2 -0
  17. package/dist/src/mcp/prompts/dsrLanding.js +40 -0
  18. package/dist/src/mcp/prompts/emailTemplate.d.ts +2 -0
  19. package/dist/src/mcp/prompts/emailTemplate.js +34 -0
  20. package/dist/src/mcp/prompts/registry.d.ts +2 -0
  21. package/dist/src/mcp/prompts/registry.js +4 -0
  22. package/dist/src/mcp/prompts/shared.d.ts +2 -0
  23. package/dist/src/mcp/prompts/shared.js +26 -0
  24. package/dist/src/mcp/prompts/types.d.ts +9 -0
  25. package/dist/src/mcp/prompts/types.js +1 -0
  26. package/dist/src/mcp/registry.d.ts +2 -0
  27. package/dist/src/mcp/registry.js +46 -0
  28. package/dist/src/mcp/resources/contentDesign.d.ts +3 -0
  29. package/dist/src/mcp/resources/contentDesign.js +62 -0
  30. package/dist/src/mcp/resources/registry.d.ts +2 -0
  31. package/dist/src/mcp/resources/registry.js +3 -0
  32. package/dist/src/mcp/resources/types.d.ts +8 -0
  33. package/dist/src/mcp/resources/types.js +1 -0
  34. package/dist/src/mcp/result.d.ts +14 -0
  35. package/dist/src/mcp/result.js +80 -0
  36. package/dist/src/mcp/serve.d.ts +4 -0
  37. package/dist/src/mcp/serve.js +18 -0
  38. package/dist/src/mcp/server.d.ts +6 -0
  39. package/dist/src/mcp/server.js +88 -0
  40. package/dist/src/mcp/tools/analytics.d.ts +2 -0
  41. package/dist/src/mcp/tools/analytics.js +20 -0
  42. package/dist/src/mcp/tools/announcements.d.ts +2 -0
  43. package/dist/src/mcp/tools/announcements.js +32 -0
  44. package/dist/src/mcp/tools/canvases.d.ts +2 -0
  45. package/dist/src/mcp/tools/canvases.js +97 -0
  46. package/dist/src/mcp/tools/devSync.d.ts +2 -0
  47. package/dist/src/mcp/tools/devSync.js +53 -0
  48. package/dist/src/mcp/tools/dsr.d.ts +2 -0
  49. package/dist/src/mcp/tools/dsr.js +47 -0
  50. package/dist/src/mcp/tools/dsrLanding.d.ts +2 -0
  51. package/dist/src/mcp/tools/dsrLanding.js +53 -0
  52. package/dist/src/mcp/tools/emailTemplate.d.ts +2 -0
  53. package/dist/src/mcp/tools/emailTemplate.js +51 -0
  54. package/dist/src/mcp/tools/files.d.ts +2 -0
  55. package/dist/src/mcp/tools/files.js +65 -0
  56. package/dist/src/mcp/tools/folders.d.ts +2 -0
  57. package/dist/src/mcp/tools/folders.js +56 -0
  58. package/dist/src/mcp/tools/groups.d.ts +2 -0
  59. package/dist/src/mcp/tools/groups.js +60 -0
  60. package/dist/src/mcp/tools/instances.d.ts +2 -0
  61. package/dist/src/mcp/tools/instances.js +82 -0
  62. package/dist/src/mcp/tools/invitations.d.ts +2 -0
  63. package/dist/src/mcp/tools/invitations.js +23 -0
  64. package/dist/src/mcp/tools/memberships.d.ts +2 -0
  65. package/dist/src/mcp/tools/memberships.js +47 -0
  66. package/dist/src/mcp/tools/metadataTemplate.d.ts +2 -0
  67. package/dist/src/mcp/tools/metadataTemplate.js +42 -0
  68. package/dist/src/mcp/tools/org.d.ts +2 -0
  69. package/dist/src/mcp/tools/org.js +12 -0
  70. package/dist/src/mcp/tools/publishApp.d.ts +2 -0
  71. package/dist/src/mcp/tools/publishApp.js +39 -0
  72. package/dist/src/mcp/tools/roles.d.ts +2 -0
  73. package/dist/src/mcp/tools/roles.js +15 -0
  74. package/dist/src/mcp/tools/search.d.ts +2 -0
  75. package/dist/src/mcp/tools/search.js +16 -0
  76. package/dist/src/mcp/tools/sharedLinks.d.ts +2 -0
  77. package/dist/src/mcp/tools/sharedLinks.js +27 -0
  78. package/dist/src/mcp/tools/tags.d.ts +2 -0
  79. package/dist/src/mcp/tools/tags.js +20 -0
  80. package/dist/src/mcp/tools/themes.d.ts +2 -0
  81. package/dist/src/mcp/tools/themes.js +28 -0
  82. package/dist/src/mcp/tools/types.d.ts +15 -0
  83. package/dist/src/mcp/tools/types.js +1 -0
  84. package/dist/src/mcp/tools/users.d.ts +2 -0
  85. package/dist/src/mcp/tools/users.js +31 -0
  86. package/package.json +9 -3
package/README.md CHANGED
@@ -10,6 +10,55 @@ p --help
10
10
  p command --help
11
11
  ```
12
12
 
13
+ ## MCP server
14
+
15
+ Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps 50 tools, one per CLI `run*` function: every command above (including `dsr create` + `dsr bind-landing`) except the five local `scaffold` subcommands (which just print/write a starter file and make no request). `dev-sync` is exposed as a one-shot sync only; the continuous `watch` mode is not available as a tool.
16
+
17
+ ```bash
18
+ p mcp
19
+ # or without a global install
20
+ npx @pitcher/cli-scripts mcp
21
+ ```
22
+
23
+ ### Environment variables
24
+
25
+ Auth and org scope are resolved once at boot from environment variables. They are never tool inputs, so the model cannot change org or credentials mid-session.
26
+
27
+ - `NG_API_KEY` (required, secret): the API key the server authenticates as.
28
+ - `NG_ORG` (default `dev`): organization subdomain, or set `NG_URL` directly for a full origin.
29
+ - `NG_ENV` (`prod` or `test`): controls the destructive-hint guard on write tools (see below). Defaults to a guess based on `NG_ORG` when unset.
30
+
31
+ One config entry = one org, because the API key is org-specific. To work against multiple orgs, add multiple entries (e.g. `pitcher-dev`, `pitcher-prod`) with different env blocks; do not try to pass org per tool call.
32
+
33
+ ### Host config example (Claude Desktop / `.mcp.json`)
34
+
35
+ ```jsonc
36
+ "pitcher": {
37
+ "command": "npx",
38
+ "args": ["-y", "@pitcher/cli-scripts", "mcp"],
39
+ "env": { "NG_API_KEY": "<key>", "NG_ORG": "dev" }
40
+ }
41
+ ```
42
+
43
+ ### Tool annotations
44
+
45
+ - Read commands are marked `readOnlyHint`. `dev-sync` counts as a write (it mutates remote dev-override state).
46
+ - `publish-app` is always marked `destructiveHint` (it creates a live, org-visible release). Other write tools get `destructiveHint` only when the resolved environment is production (`NG_ENV=prod`, or `NG_ORG` other than `dev` with `NG_ENV` unset). This steers hosts toward requiring extra confirmation before mutating a production org.
47
+
48
+ ### Desktop Extension (`.mcpb`)
49
+
50
+ For one-click install in Claude Desktop (name + icon, an install-time API-key/org form, no hand-edited config, no Node prerequisite), build a `.mcpb` bundle:
51
+
52
+ ```bash
53
+ yarn mcpb:pack # → pitcher.mcpb (validated + packed)
54
+ ```
55
+
56
+ Install by double-clicking `pitcher.mcpb` (or Settings → Extensions → Install Extension). Claude Desktop prompts for the API key (stored in the OS keychain) and organization; both are editable afterward, so switching orgs needs no reinstall.
57
+
58
+ The bundle reuses the same server as `p mcp` but registers a **content-tool subset (47)** — it omits the developer-only `publish-app` and `dev-sync`/`dev-sync-revert`. The `organization` field accepts a subdomain (`acme`), a bare hostname (`acme.my.pitcher.com`), or a full `https://` URL. Build details (esbuild single-file, version injection) live in `mcpb/` and `CLAUDE.md`.
59
+
60
+ This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the `pitcher-apps` skill, not this README.
61
+
13
62
  ## To run the `publish-app` command:
14
63
 
15
64
  ```bash
@@ -207,6 +256,9 @@ Create/list/get HTML email templates for the emails app. `create`, `list`, and `
207
256
  # Create a template from an HTML file (inline CSS)
208
257
  p email-template create --instance-id 01... --name "Welcome" --body-file ./welcome.html --api-key myapikey
209
258
 
259
+ # ...or pass the HTML inline instead of a file (use one, not both)
260
+ p email-template create --instance-id 01... --name "Welcome" --body-html "<p>Hi {{recipient.firstName}}</p>" --api-key myapikey
261
+
210
262
  # List templates (optionally filter by name/status/category/target)
211
263
  p email-template list --instance-id 01... --api-key myapikey
212
264
 
@@ -229,6 +281,9 @@ Create/list DSR (Digital Sales Room) landing-page templates, and set one as the
229
281
  # Create a landing template from an HTML file (must contain the email-gate marker)
230
282
  p dsr-landing create --instance-id 01... --name "Default" --html-file ./landing.html --api-key myapikey
231
283
 
284
+ # ...or pass the HTML (and optionally CSS) inline instead of a file (use one, not both)
285
+ p dsr-landing create --instance-id 01... --name "Default" --html "<div data-pitcher-email-gate></div>" --api-key myapikey
286
+
232
287
  # Create and set as default in one call
233
288
  p dsr-landing create --instance-id 01... --name "Default" --html-file ./landing.html --default --api-key myapikey
234
289
 
@@ -242,7 +297,7 @@ p dsr-landing list --instance-id 01... --api-key myapikey
242
297
  p dsr-landing scaffold --out ./landing.html --api-key myapikey
243
298
  ```
244
299
 
245
- `--html-file` must contain the `data-pitcher-email-gate` marker verbatim (the platform injects the email-gate form there) — `create` fails client-side before any request if it's missing.
300
+ The landing HTML (whether via `--html-file` or `--html`) must contain the `data-pitcher-email-gate` marker verbatim (the platform injects the email-gate form there) — `create` fails client-side before any request if it's missing. Provide exactly one of `--html` / `--html-file` (and at most one of `--css` / `--css-file`).
246
301
 
247
302
  Run `p dsr-landing create --help` for the full token/marker reference (editable-region attributes, recipient/customer/org/rep tokens).
248
303
 
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { createGroupsCommand } from './src/commands/groups.js';
25
25
  import { createRolesCommand } from './src/commands/roles.js';
26
26
  import { createMetadataTemplateCommand } from './src/commands/metadataTemplate.js';
27
27
  import { createDsrCommand } from './src/commands/dsr.js';
28
+ import { createMcpCommand } from './src/commands/mcp.js';
28
29
  const __filename = fileURLToPath(import.meta.url);
29
30
  const __dirname = dirname(__filename);
30
31
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -57,4 +58,5 @@ program.addCommand(createMetadataTemplateCommand());
57
58
  program.addCommand(createDsrCommand());
58
59
  program.addCommand(createOrgCommand());
59
60
  program.addCommand(createAnalyticsCommand());
61
+ program.addCommand(createMcpCommand());
60
62
  program.parseAsync(process.argv);
@@ -13,14 +13,17 @@ export async function runCanvasesList(opts, deps = {}) {
13
13
  assertIntFlags(opts);
14
14
  const client = clientFor(opts, deps);
15
15
  const params = { instance_id: opts.instanceId, ...listPageParams(opts) };
16
- // Send each type flag ONLY when set. The server FilterSet does
17
- // setdefault("is_block", False), so blocks are hidden unless --is-block.
18
- if (opts.isTemplate)
19
- params.is_template = true;
20
- if (opts.isSection)
21
- params.is_section = true;
22
- if (opts.isBlock)
23
- params.is_block = true;
16
+ // Forward each type flag only when explicitly set (true OR false) — the server honors
17
+ // is_section=false / is_template=false for precise type filtering. CLI switches are
18
+ // present→true / absent→undefined, so this is behavior-preserving for the CLI; it lets a
19
+ // programmatic caller (the MCP `type` filter) isolate one canvas type. The server FilterSet
20
+ // does setdefault("is_block", False), so blocks stay hidden unless is_block is set.
21
+ if (opts.isTemplate !== undefined)
22
+ params.is_template = opts.isTemplate;
23
+ if (opts.isSection !== undefined)
24
+ params.is_section = opts.isSection;
25
+ if (opts.isBlock !== undefined)
26
+ params.is_block = opts.isBlock;
24
27
  if (opts.tags)
25
28
  params.tags__in = opts.tags;
26
29
  if (opts.idIn)
@@ -8,7 +8,9 @@ export interface DsrLandingDeps {
8
8
  export declare function runDsrLandingCreate(opts: CommonOpts & {
9
9
  instanceId?: string;
10
10
  name?: string;
11
+ html?: string;
11
12
  htmlFile?: string;
13
+ css?: string;
12
14
  cssFile?: string;
13
15
  mergeSchema?: string;
14
16
  thumbnailUrl?: string;
@@ -3,7 +3,7 @@ import { clientFor, toCliError } from '../core/http.js';
3
3
  import { ok, fail } from '../core/envelope.js';
4
4
  import { createCliError } from '../core/errors.js';
5
5
  import { emitEnvelope } from '../core/output.js';
6
- import { readTextFile } from '../core/readInput.js';
6
+ import { resolveTextInput } from '../core/readInput.js';
7
7
  import { parseJsonArray } from '../schema/content.js';
8
8
  import { withAuthFlags } from './withAuth.js';
9
9
  import { withInstanceFlag } from './withInstance.js';
@@ -12,7 +12,7 @@ const DSR_ENDPOINT = '/core/api/protected/dsr-landing-templates';
12
12
  const REQUIRED_MARKER = 'data-pitcher-email-gate';
13
13
  const TOKEN_HELP = `
14
14
  Required marker (the platform injects the email-gate form here at render time):
15
- data-pitcher-email-gate must be present verbatim in --html-file, or create fails client-side
15
+ data-pitcher-email-gate must be present verbatim in the landing HTML (--html or --html-file), or create fails client-side
16
16
 
17
17
  Editable regions (optional, edited in the DSR landing editor without touching markup):
18
18
  data-field-id="..." unique id for the editable block
@@ -33,10 +33,15 @@ export async function runDsrLandingCreate(opts, deps = {}) {
33
33
  throw createCliError('VALIDATION', '--name is required for dsr-landing create');
34
34
  if (!opts.instanceId)
35
35
  throw createCliError('VALIDATION', '--instance-id is required for dsr-landing create');
36
- if (!opts.htmlFile)
37
- throw createCliError('VALIDATION', '--html-file is required for dsr-landing create');
38
- // readTextFile throws VALIDATION on a missing/unreadable file, before any request is made.
39
- const html = readTextFile(opts.htmlFile);
36
+ // Inline HTML (html) OR a file path (htmlFile), not both — exactly one is required.
37
+ // Inline is the only route that works in hosts without a shared filesystem (e.g. Claude
38
+ // Desktop). resolveTextInput throws VALIDATION on conflict/oversize/unreadable.
39
+ const html = resolveTextInput(opts.html, opts.htmlFile, 'html');
40
+ if (html === undefined) {
41
+ // host-neutral wording: the MCP tool params are html/htmlFile, the CLI flags are --html/--html-file
42
+ throw createCliError('VALIDATION', 'provide the landing HTML inline or as a file (html / htmlFile) for dsr-landing create');
43
+ }
44
+ // Marker check runs on the resolved HTML regardless of source.
40
45
  if (!html.includes(REQUIRED_MARKER)) {
41
46
  throw createCliError('VALIDATION', `landing HTML must contain the ${REQUIRED_MARKER} marker`);
42
47
  }
@@ -44,8 +49,9 @@ export async function runDsrLandingCreate(opts, deps = {}) {
44
49
  throw createCliError('VALIDATION', '--language must be at most 10 characters');
45
50
  }
46
51
  const body = { instance_id: opts.instanceId, name: opts.name, html };
47
- if (opts.cssFile)
48
- body.css = readTextFile(opts.cssFile);
52
+ const css = resolveTextInput(opts.css, opts.cssFile, 'css');
53
+ if (css !== undefined)
54
+ body.css = css;
49
55
  if (opts.mergeSchema)
50
56
  body.merge_schema = parseJsonArray(opts.mergeSchema, 'merge-schema');
51
57
  if (opts.thumbnailUrl)
@@ -117,8 +123,10 @@ export function createDsrLandingCommand() {
117
123
  const cmd = new Command('dsr-landing').description('create/list DSR (Digital Sales Room) landing-page templates, and set one as default');
118
124
  withInstanceFlag(withAuthFlags(cmd.command('create')))
119
125
  .option('--name <name>', 'Template name (required)')
120
- .option('--html-file <path>', `Path to the landing HTML (required; must contain ${REQUIRED_MARKER})`)
121
- .option('--css-file <path>', 'Path to a CSS file for the template')
126
+ .option('--html <html>', `Inline landing HTML (must contain ${REQUIRED_MARKER}). Use this OR --html-file`)
127
+ .option('--html-file <path>', `Path to the landing HTML (must contain ${REQUIRED_MARKER}). Use this OR --html`)
128
+ .option('--css <css>', 'Inline CSS for the template. Use this OR --css-file')
129
+ .option('--css-file <path>', 'Path to a CSS file for the template. Use this OR --css')
122
130
  .option('--merge-schema <json>', 'Merge schema array as inline JSON')
123
131
  .option('--language <lang>', 'Language code (max 10 chars)')
124
132
  .option('--thumbnail-url <url>', 'Thumbnail image URL')
@@ -8,6 +8,7 @@ export interface EmailTemplateDeps {
8
8
  export declare function runEmailTemplateCreate(opts: CommonOpts & {
9
9
  instanceId?: string;
10
10
  name?: string;
11
+ bodyHtml?: string;
11
12
  bodyFile?: string;
12
13
  subject?: string;
13
14
  tags?: string;
@@ -3,7 +3,7 @@ import { clientFor, toCliError } from '../core/http.js';
3
3
  import { ok, fail } from '../core/envelope.js';
4
4
  import { createCliError } from '../core/errors.js';
5
5
  import { emitEnvelope } from '../core/output.js';
6
- import { readTextFile } from '../core/readInput.js';
6
+ import { resolveTextInput } from '../core/readInput.js';
7
7
  import { listLimitOffsetParams } from '../core/pagination.js';
8
8
  import { assertIntFlags, parseJsonObject } from '../schema/content.js';
9
9
  import { withAuthFlags } from './withAuth.js';
@@ -26,9 +26,12 @@ export async function runEmailTemplateCreate(opts, deps = {}) {
26
26
  const body = { name: opts.name };
27
27
  if (opts.subject)
28
28
  body.subject = opts.subject;
29
- // readTextFile throws VALIDATION on a missing/unreadable file, before any request is made.
30
- if (opts.bodyFile)
31
- body.body = readTextFile(opts.bodyFile);
29
+ // Inline body (bodyHtml) OR a file path (bodyFile), not both. Inline is the only
30
+ // route that works in hosts without a shared filesystem (e.g. Claude Desktop).
31
+ // resolveTextInput throws VALIDATION on conflict/oversize/unreadable, before any request.
32
+ const bodyHtml = resolveTextInput(opts.bodyHtml, opts.bodyFile, 'body');
33
+ if (bodyHtml !== undefined)
34
+ body.body = bodyHtml;
32
35
  if (opts.tags)
33
36
  body.tags = parseJsonObject(opts.tags, 'tags');
34
37
  if (opts.metadata)
@@ -86,6 +89,7 @@ export function createEmailTemplateCommand() {
86
89
  const cmd = new Command('email-template').description('create/list/get HTML email templates for the emails app');
87
90
  withInstanceFlag(withAuthFlags(cmd.command('create')))
88
91
  .option('--name <name>', 'Template name (required)')
92
+ .option('--body-html <html>', 'Inline HTML body (inline CSS). Use this OR --body-file')
89
93
  .option('--body-file <path>', 'Path to an HTML file (inline CSS) used as the template body')
90
94
  .option('--subject <text>', 'Email subject (supports {{tokens}})')
91
95
  .option('--tags <json>', 'Tags object as inline JSON')
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createMcpCommand(): Command;
@@ -0,0 +1,18 @@
1
+ import { Command } from 'commander';
2
+ import { runMcpServe } from '../mcp/serve.js';
3
+ import { isCliError } from '../core/errors.js';
4
+ export function createMcpCommand() {
5
+ return new Command('mcp')
6
+ .description('Run the Pitcher MCP server over stdio (reads NG_API_KEY, NG_ORG/NG_URL, NG_ENV from env)')
7
+ .action(async () => {
8
+ try {
9
+ await runMcpServe();
10
+ }
11
+ catch (e) {
12
+ const message = isCliError(e) ? e.message : e instanceof Error ? e.message : String(e);
13
+ console.error(`[pitcher-mcp] failed to start: ${message}`);
14
+ process.exitCode = isCliError(e) ? e.exitCode : 1;
15
+ return;
16
+ }
17
+ });
18
+ }
@@ -1,2 +1,4 @@
1
1
  export declare function readTextFile(filePath: string): string;
2
2
  export declare function readJsonFile(filePath: string): unknown;
3
+ export declare const MAX_INLINE_BYTES = 400000;
4
+ export declare function resolveTextInput(inline: string | undefined, file: string | undefined, label: string): string | undefined;
@@ -21,3 +21,27 @@ export function readJsonFile(filePath) {
21
21
  throw createCliError('VALIDATION', `invalid JSON in ${filePath}`);
22
22
  }
23
23
  }
24
+ // Cap inbound inline content so a runaway model-generated payload fails cleanly
25
+ // client-side (a clear VALIDATION) instead of ballooning memory or POSTing a huge body
26
+ // that the platform rejects with an opaque 4xx. Bytes, not chars (UTF-8).
27
+ export const MAX_INLINE_BYTES = 400000;
28
+ // Resolve a text field from either an inline string or a file path (exactly one).
29
+ // Truthy guards (not `!== undefined`) preserve the pre-existing falsy-skip behavior:
30
+ // an empty inline string or empty path counts as "not provided", matching the old
31
+ // `if (opts.bodyFile)` / `if (opts.htmlFile)` checks. Returns undefined if neither.
32
+ // The size cap applies only to inline content — a file path is disk-bounded already,
33
+ // and capping it could break existing large-file callers.
34
+ export function resolveTextInput(inline, file, label) {
35
+ if (inline && file) {
36
+ throw createCliError('VALIDATION', `provide either an inline ${label} or a ${label} file, not both`);
37
+ }
38
+ if (inline) {
39
+ if (Buffer.byteLength(inline, 'utf-8') > MAX_INLINE_BYTES) {
40
+ throw createCliError('VALIDATION', `inline ${label} exceeds ${MAX_INLINE_BYTES} bytes`);
41
+ }
42
+ return inline;
43
+ }
44
+ if (file)
45
+ return readTextFile(file);
46
+ return undefined;
47
+ }
@@ -0,0 +1,4 @@
1
+ import type { ToolDescriptor } from './tools/types.js';
2
+ export declare const BUNDLE_EXCLUDED_TOOLS: Set<string>;
3
+ export declare const bundledTools: ToolDescriptor[];
4
+ export declare function normalizeOrgUrl(env: NodeJS.ProcessEnv): void;
@@ -0,0 +1,25 @@
1
+ import { allTools } from './registry.js';
2
+ // Developer-only tools excluded from the consumer .mcpb bundle. `publish_app` is alwaysDestructive
3
+ // (creates a live, org-visible app release); `dev_sync`/`dev_sync_revert` mutate remote S3 dev state.
4
+ // None are relevant to a non-technical adopter installing the extension. The CLI `p mcp` keeps all 50.
5
+ export const BUNDLE_EXCLUDED_TOOLS = new Set(['pitcher_publish_app', 'pitcher_dev_sync', 'pitcher_dev_sync_revert']);
6
+ // The tool set the .mcpb bundle registers.
7
+ export const bundledTools = allTools.filter((t) => !BUNDLE_EXCLUDED_TOOLS.has(t.name));
8
+ // resolveBaseUrl interpolates NG_ORG into `https://<org>.my.pitcher.com`, so anything that is
9
+ // already a host (a full URL, or a bare hostname like "acme.my.pitcher.com") must route via NG_URL
10
+ // instead — otherwise it becomes a double-interpolated garbage host. The .mcpb install form accepts
11
+ // a subdomain, hostname, or full URL in one `org` field; this moves host-shaped values to NG_URL
12
+ // before the server reads env. A plain subdomain (no dot) stays in NG_ORG. Mutates env in place.
13
+ export function normalizeOrgUrl(env) {
14
+ const org = env.NG_ORG?.trim();
15
+ if (!org)
16
+ return;
17
+ if (/^https?:\/\//i.test(org)) {
18
+ env.NG_URL = org;
19
+ delete env.NG_ORG;
20
+ }
21
+ else if (org.includes('.')) {
22
+ env.NG_URL = `https://${org}`;
23
+ delete env.NG_ORG;
24
+ }
25
+ }
@@ -0,0 +1,7 @@
1
+ export interface McpEnv {
2
+ apiKey: string;
3
+ org?: string;
4
+ url?: string;
5
+ prod: boolean;
6
+ }
7
+ export declare function resolveMcpEnv(env?: NodeJS.ProcessEnv): McpEnv;
@@ -0,0 +1,13 @@
1
+ import { createCliError } from '../core/errors.js';
2
+ export function resolveMcpEnv(env = process.env) {
3
+ const apiKey = env.NG_API_KEY?.trim();
4
+ if (!apiKey) {
5
+ throw createCliError('AUTH', 'NG_API_KEY is required to run the MCP server (set it in the host config env).');
6
+ }
7
+ const org = env.NG_ORG?.trim() || 'dev';
8
+ const url = env.NG_URL?.trim() || undefined;
9
+ const ngEnv = env.NG_ENV?.trim();
10
+ const isDevHost = url ? /\/\/dev\.my\.pitcher\.com(\/|$)/.test(url) : org === 'dev';
11
+ const prod = ngEnv === 'prod' ? true : ngEnv === 'test' ? false : !isDevHost;
12
+ return { apiKey, org, url, prod };
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { PromptDescriptor } from './types.js';
2
+ export declare const dsrLandingPrompts: PromptDescriptor[];
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import { instanceLine, recipePreamble } from './shared.js';
3
+ export const dsrLandingPrompts = [
4
+ {
5
+ name: 'seed_dsr_landing',
6
+ title: 'Create DSR landing page',
7
+ description: 'Author a responsive DSR (Digital Sales Room) landing-page template and create it in an instance.',
8
+ argsSchema: {
9
+ instance_id: z.string().optional().describe('Target instance ID (leave blank to resolve interactively)'),
10
+ vertical: z.string().optional().describe('pharma | medtech | cpg | finserv | generic'),
11
+ set_default: z.string().optional().describe('"true" to also set it as the instance default'),
12
+ },
13
+ build: (args) => {
14
+ const vertical = args.vertical || 'generic';
15
+ const setDefault = args.set_default === 'true';
16
+ const text = [
17
+ recipePreamble(),
18
+ '',
19
+ '## Task: seed a DSR landing page',
20
+ instanceLine(args.instance_id),
21
+ `Vertical: ${vertical}.`,
22
+ '',
23
+ 'Steps:',
24
+ '1. Draft a responsive landing HTML following the principles above. It **must contain the ' +
25
+ '`data-pitcher-email-gate` marker** (the platform injects the email-gate form there). Use ' +
26
+ '`clamp()` for fluid sizing (no media queries), guard `{{rep.*}}` merge fields with fallbacks, ' +
27
+ 'and pick an aesthetic that fits the vertical.',
28
+ '2. Create it by calling `pitcher_dsr_landing_create` with ' +
29
+ `\`{ instanceId, name, html${setDefault ? ', default: true' : ''} }\` — pass the HTML **inline via ` +
30
+ '`html`**, not a file path (inline is the only route that works in hosts without a shared filesystem).',
31
+ setDefault
32
+ ? ' (`set_default` was "true" → pass `default: true` so it becomes the instance default.)'
33
+ : ' (Leave `default` unset — the caller did not ask to set it as the instance default.)',
34
+ '3. The create endpoint is editor-gated; if it returns 403, surface it as-is. Report the created ' +
35
+ 'template id and, if applicable, the DSR link.',
36
+ ].join('\n');
37
+ return [{ role: 'user', content: { type: 'text', text } }];
38
+ },
39
+ },
40
+ ];
@@ -0,0 +1,2 @@
1
+ import type { PromptDescriptor } from './types.js';
2
+ export declare const emailTemplatePrompts: PromptDescriptor[];
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import { instanceLine, recipePreamble } from './shared.js';
3
+ export const emailTemplatePrompts = [
4
+ {
5
+ name: 'seed_email_template',
6
+ title: 'Create email template',
7
+ description: 'Author an on-brand HTML email template for the emails app and create it in an instance.',
8
+ argsSchema: {
9
+ instance_id: z.string().optional().describe('Target instance ID (leave blank to resolve interactively)'),
10
+ vertical: z.string().optional().describe('pharma | medtech | cpg | finserv | generic'),
11
+ use_case: z.string().optional().describe('Short description of what this email is for'),
12
+ },
13
+ build: (args) => {
14
+ const vertical = args.vertical || 'generic';
15
+ const useCase = args.use_case || 'a general product-update email a rep would send to a customer';
16
+ const text = [
17
+ recipePreamble(),
18
+ '',
19
+ '## Task: seed an email template',
20
+ instanceLine(args.instance_id),
21
+ `Vertical: ${vertical}. Use case: ${useCase}.`,
22
+ '',
23
+ 'Steps:',
24
+ '1. Draft compliant inline-CSS HTML following the principles above: max ~620px, table layout, ' +
25
+ 'one clear CTA, visually distinct editable fields, and a vertical-appropriate greeting and ' +
26
+ 'compliance footer.',
27
+ '2. Create it by calling `pitcher_email_template_create` with `{ instanceId, name, bodyHtml }` — ' +
28
+ 'pass the HTML **inline via `bodyHtml`** (a file path cannot cross a host\'s sandbox boundary, so always inline).',
29
+ '3. Report the created template id and name.',
30
+ ].join('\n');
31
+ return [{ role: 'user', content: { type: 'text', text } }];
32
+ },
33
+ },
34
+ ];
@@ -0,0 +1,2 @@
1
+ import type { PromptDescriptor } from './types.js';
2
+ export declare const allPrompts: PromptDescriptor[];
@@ -0,0 +1,4 @@
1
+ import { emailTemplatePrompts } from './emailTemplate.js';
2
+ import { dsrLandingPrompts } from './dsrLanding.js';
3
+ // Every recipe prompt the server can expose. Mirrors mcp/registry.ts (allTools).
4
+ export const allPrompts = [...emailTemplatePrompts, ...dsrLandingPrompts];
@@ -0,0 +1,2 @@
1
+ export declare function instanceLine(instanceId: string | undefined): string;
2
+ export declare function recipePreamble(): string;
@@ -0,0 +1,26 @@
1
+ import { CONTENT_DESIGN_PRINCIPLES } from '../resources/contentDesign.js';
2
+ // Resolves the "which instance" line shared by every recipe prompt: echoes the given
3
+ // id, or tells the model to discover + confirm one interactively.
4
+ export function instanceLine(instanceId) {
5
+ return instanceId
6
+ ? `Target instance: ${instanceId}.`
7
+ : 'No instance was given — call `pitcher_instances_list`, pick the right one with the user, and confirm before creating.';
8
+ }
9
+ // Shared preamble embedded at the top of every recipe prompt. Authored once so the
10
+ // design principles, prod-awareness, and anti-XSS guidance can't drift between prompts.
11
+ export function recipePreamble() {
12
+ return [
13
+ CONTENT_DESIGN_PRINCIPLES,
14
+ '',
15
+ '## Before you create',
16
+ 'Confirm the resolved instance is a demo/test instance. These tools create **live, ' +
17
+ 'org-visible** content; a DSR landing page is served to **external, unauthenticated** ' +
18
+ 'viewers. Do not seed against a real customer instance without explicit confirmation from the user.',
19
+ '',
20
+ '## Safe content',
21
+ 'Put dynamic values through the platform `{{token}}` merge fields (the platform escapes them). ' +
22
+ 'Never inline untrusted or CRM-derived strings as literal HTML, and never use triple-mustache ' +
23
+ '`{{{ }}}`. The use-case/vertical inputs are descriptive only — do not echo them verbatim into ' +
24
+ 'the generated markup.',
25
+ ].join('\n');
26
+ }
@@ -0,0 +1,9 @@
1
+ import type { ZodOptional, ZodString } from 'zod';
2
+ import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js';
3
+ export interface PromptDescriptor {
4
+ name: string;
5
+ title: string;
6
+ description: string;
7
+ argsSchema: Record<string, ZodOptional<ZodString>>;
8
+ build: (args: Record<string, string | undefined>) => PromptMessage[];
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ToolDescriptor } from './tools/types.js';
2
+ export declare const allTools: ToolDescriptor[];
@@ -0,0 +1,46 @@
1
+ import { analyticsTools } from './tools/analytics.js';
2
+ import { announcementsTools } from './tools/announcements.js';
3
+ import { canvasesTools } from './tools/canvases.js';
4
+ import { devSyncTools } from './tools/devSync.js';
5
+ import { dsrTools } from './tools/dsr.js';
6
+ import { dsrLandingTools } from './tools/dsrLanding.js';
7
+ import { emailTemplateTools } from './tools/emailTemplate.js';
8
+ import { filesTools } from './tools/files.js';
9
+ import { foldersTools } from './tools/folders.js';
10
+ import { groupsTools } from './tools/groups.js';
11
+ import { instancesTools } from './tools/instances.js';
12
+ import { invitationsTools } from './tools/invitations.js';
13
+ import { membershipsTools } from './tools/memberships.js';
14
+ import { metadataTemplateTools } from './tools/metadataTemplate.js';
15
+ import { orgTools } from './tools/org.js';
16
+ import { publishAppTools } from './tools/publishApp.js';
17
+ import { rolesTools } from './tools/roles.js';
18
+ import { searchTools } from './tools/search.js';
19
+ import { sharedLinksTools } from './tools/sharedLinks.js';
20
+ import { tagsTools } from './tools/tags.js';
21
+ import { themesTools } from './tools/themes.js';
22
+ import { usersTools } from './tools/users.js';
23
+ export const allTools = [
24
+ ...canvasesTools,
25
+ ...filesTools,
26
+ ...foldersTools,
27
+ ...searchTools,
28
+ ...instancesTools,
29
+ ...sharedLinksTools,
30
+ ...tagsTools,
31
+ ...announcementsTools,
32
+ ...dsrLandingTools,
33
+ ...dsrTools,
34
+ ...emailTemplateTools,
35
+ ...themesTools,
36
+ ...usersTools,
37
+ ...invitationsTools,
38
+ ...rolesTools,
39
+ ...orgTools,
40
+ ...analyticsTools,
41
+ ...membershipsTools,
42
+ ...groupsTools,
43
+ ...metadataTemplateTools,
44
+ ...devSyncTools,
45
+ ...publishAppTools,
46
+ ];
@@ -0,0 +1,3 @@
1
+ import type { ResourceDescriptor } from './types.js';
2
+ export declare const CONTENT_DESIGN_PRINCIPLES = "# Pitcher content design principles\n\nDistilled guidance for authoring good Pitcher demo content. Apply these when generating\nemail templates and DSR landing pages.\n\n## 1. Palette \u2014 derive, never hardcode\nBuild a 6-tone ramp from the instance primary color rather than pasting brand hex codes:\ndarken 30% and 15% for accents/headers, lighten 15/30/60/80% for backgrounds and tints.\nThis keeps content on-brand across instances automatically.\n\n## 2. HTML email standards\n- Max width ~620px, centered in the viewport.\n- Table-based layout \u2014 NOT CSS grid/flex (email clients drop them).\n- All styles inline \u2014 no `<style>` block.\n- Exactly one clear call-to-action.\n- Make editable regions obvious: wrap them in a visually distinct container (colored\n background, left border, an uppercase label above).\n- Every template needs a realistic demo story \u2014 a use case a sales rep would actually send.\n\n## 3. DSR landing-page standards\n- The HTML MUST contain the `data-pitcher-email-gate` marker (the platform injects the\n email-gate form there at render time), or creation fails client-side.\n- Responsive without media queries: use `clamp(min, fluid, max)` for sizing plus\n `flex-wrap: wrap`; that covers 320px to 1440px with no breakpoints.\n- Guard merge fields at the field level with sensible fallbacks, e.g. a default when\n `{{rep.name}}` is absent.\n\n## 4. Band / bookend layout (shared canvas vocabulary)\n- Hero: full brand fill with a white title.\n- Content bands: alternate white and a faint brand tint; use generous padding.\n- Closing: a deeper brand fill.\n- Avoid the \"washed-out trap\" \u2014 inject real brand fills instead of relying on theme\n presets that hard-code headings to black.\n\n## 5. Safe dynamic content (security \u2014 required)\nDynamic values go ONLY through the platform's `{{token}}` merge fields, which the platform\nescapes. Never inline untrusted or CRM-derived strings as literal HTML, and never use\ntriple-mustache `{{{ }}}`. DSR landing pages render to external, unauthenticated viewers,\nso unescaped dynamic HTML is a stored-XSS boundary. Descriptive inputs (a use case, a\nvertical) are for steering the design \u2014 do not echo them verbatim into the markup.\n\n## 6. Demo-story discipline (house rules)\n- Every artifact needs a realistic use case a rep would actually send.\n- No real vendor or competitor names.\n- Stats and figures are illustrative, not represented as real customer data.\n- All copy in English.\n";
3
+ export declare const contentDesignResource: ResourceDescriptor;
@@ -0,0 +1,62 @@
1
+ // Distilled Pitcher content-design nucleus. This is the SINGLE source: the resource
2
+ // below returns it, and both recipe prompts (mcp/prompts/*) embed it inline (a URI
3
+ // mention alone is not auto-fetched by MCP hosts, so prompts must carry the text).
4
+ //
5
+ // Distilled from the personal Claude Code skills `email-templates`, `dsr-landing-seed`,
6
+ // and `playbook-seed` (palette/band vocabulary). Those skills remain the authoritative
7
+ // deep version; keep this in sync on material design changes. Last synced: 2026-07-17.
8
+ export const CONTENT_DESIGN_PRINCIPLES = `# Pitcher content design principles
9
+
10
+ Distilled guidance for authoring good Pitcher demo content. Apply these when generating
11
+ email templates and DSR landing pages.
12
+
13
+ ## 1. Palette — derive, never hardcode
14
+ Build a 6-tone ramp from the instance primary color rather than pasting brand hex codes:
15
+ darken 30% and 15% for accents/headers, lighten 15/30/60/80% for backgrounds and tints.
16
+ This keeps content on-brand across instances automatically.
17
+
18
+ ## 2. HTML email standards
19
+ - Max width ~620px, centered in the viewport.
20
+ - Table-based layout — NOT CSS grid/flex (email clients drop them).
21
+ - All styles inline — no \`<style>\` block.
22
+ - Exactly one clear call-to-action.
23
+ - Make editable regions obvious: wrap them in a visually distinct container (colored
24
+ background, left border, an uppercase label above).
25
+ - Every template needs a realistic demo story — a use case a sales rep would actually send.
26
+
27
+ ## 3. DSR landing-page standards
28
+ - The HTML MUST contain the \`data-pitcher-email-gate\` marker (the platform injects the
29
+ email-gate form there at render time), or creation fails client-side.
30
+ - Responsive without media queries: use \`clamp(min, fluid, max)\` for sizing plus
31
+ \`flex-wrap: wrap\`; that covers 320px to 1440px with no breakpoints.
32
+ - Guard merge fields at the field level with sensible fallbacks, e.g. a default when
33
+ \`{{rep.name}}\` is absent.
34
+
35
+ ## 4. Band / bookend layout (shared canvas vocabulary)
36
+ - Hero: full brand fill with a white title.
37
+ - Content bands: alternate white and a faint brand tint; use generous padding.
38
+ - Closing: a deeper brand fill.
39
+ - Avoid the "washed-out trap" — inject real brand fills instead of relying on theme
40
+ presets that hard-code headings to black.
41
+
42
+ ## 5. Safe dynamic content (security — required)
43
+ Dynamic values go ONLY through the platform's \`{{token}}\` merge fields, which the platform
44
+ escapes. Never inline untrusted or CRM-derived strings as literal HTML, and never use
45
+ triple-mustache \`{{{ }}}\`. DSR landing pages render to external, unauthenticated viewers,
46
+ so unescaped dynamic HTML is a stored-XSS boundary. Descriptive inputs (a use case, a
47
+ vertical) are for steering the design — do not echo them verbatim into the markup.
48
+
49
+ ## 6. Demo-story discipline (house rules)
50
+ - Every artifact needs a realistic use case a rep would actually send.
51
+ - No real vendor or competitor names.
52
+ - Stats and figures are illustrative, not represented as real customer data.
53
+ - All copy in English.
54
+ `;
55
+ export const contentDesignResource = {
56
+ name: 'content-principles',
57
+ uri: 'pitcher://design/content-principles',
58
+ title: 'Pitcher content design principles',
59
+ description: 'Palette, HTML-email, DSR-landing, band-layout, and safe-token guidance for authoring content.',
60
+ mimeType: 'text/markdown',
61
+ read: () => CONTENT_DESIGN_PRINCIPLES,
62
+ };
@@ -0,0 +1,2 @@
1
+ import type { ResourceDescriptor } from './types.js';
2
+ export declare const allResources: ResourceDescriptor[];