@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.
- package/README.md +56 -1
- package/dist/index.js +2 -0
- package/dist/src/commands/canvases.js +11 -8
- package/dist/src/commands/dsrLanding.d.ts +2 -0
- package/dist/src/commands/dsrLanding.js +18 -10
- package/dist/src/commands/emailTemplate.d.ts +1 -0
- package/dist/src/commands/emailTemplate.js +8 -4
- package/dist/src/commands/mcp.d.ts +2 -0
- package/dist/src/commands/mcp.js +18 -0
- package/dist/src/core/readInput.d.ts +2 -0
- package/dist/src/core/readInput.js +24 -0
- package/dist/src/mcp/bundled.d.ts +4 -0
- package/dist/src/mcp/bundled.js +25 -0
- package/dist/src/mcp/env.d.ts +7 -0
- package/dist/src/mcp/env.js +13 -0
- package/dist/src/mcp/prompts/dsrLanding.d.ts +2 -0
- package/dist/src/mcp/prompts/dsrLanding.js +40 -0
- package/dist/src/mcp/prompts/emailTemplate.d.ts +2 -0
- package/dist/src/mcp/prompts/emailTemplate.js +34 -0
- package/dist/src/mcp/prompts/registry.d.ts +2 -0
- package/dist/src/mcp/prompts/registry.js +4 -0
- package/dist/src/mcp/prompts/shared.d.ts +2 -0
- package/dist/src/mcp/prompts/shared.js +26 -0
- package/dist/src/mcp/prompts/types.d.ts +9 -0
- package/dist/src/mcp/prompts/types.js +1 -0
- package/dist/src/mcp/registry.d.ts +2 -0
- package/dist/src/mcp/registry.js +46 -0
- package/dist/src/mcp/resources/contentDesign.d.ts +3 -0
- package/dist/src/mcp/resources/contentDesign.js +62 -0
- package/dist/src/mcp/resources/registry.d.ts +2 -0
- package/dist/src/mcp/resources/registry.js +3 -0
- package/dist/src/mcp/resources/types.d.ts +8 -0
- package/dist/src/mcp/resources/types.js +1 -0
- package/dist/src/mcp/result.d.ts +14 -0
- package/dist/src/mcp/result.js +80 -0
- package/dist/src/mcp/serve.d.ts +4 -0
- package/dist/src/mcp/serve.js +18 -0
- package/dist/src/mcp/server.d.ts +6 -0
- package/dist/src/mcp/server.js +88 -0
- package/dist/src/mcp/tools/analytics.d.ts +2 -0
- package/dist/src/mcp/tools/analytics.js +20 -0
- package/dist/src/mcp/tools/announcements.d.ts +2 -0
- package/dist/src/mcp/tools/announcements.js +32 -0
- package/dist/src/mcp/tools/canvases.d.ts +2 -0
- package/dist/src/mcp/tools/canvases.js +97 -0
- package/dist/src/mcp/tools/devSync.d.ts +2 -0
- package/dist/src/mcp/tools/devSync.js +53 -0
- package/dist/src/mcp/tools/dsr.d.ts +2 -0
- package/dist/src/mcp/tools/dsr.js +47 -0
- package/dist/src/mcp/tools/dsrLanding.d.ts +2 -0
- package/dist/src/mcp/tools/dsrLanding.js +53 -0
- package/dist/src/mcp/tools/emailTemplate.d.ts +2 -0
- package/dist/src/mcp/tools/emailTemplate.js +51 -0
- package/dist/src/mcp/tools/files.d.ts +2 -0
- package/dist/src/mcp/tools/files.js +65 -0
- package/dist/src/mcp/tools/folders.d.ts +2 -0
- package/dist/src/mcp/tools/folders.js +56 -0
- package/dist/src/mcp/tools/groups.d.ts +2 -0
- package/dist/src/mcp/tools/groups.js +60 -0
- package/dist/src/mcp/tools/instances.d.ts +2 -0
- package/dist/src/mcp/tools/instances.js +82 -0
- package/dist/src/mcp/tools/invitations.d.ts +2 -0
- package/dist/src/mcp/tools/invitations.js +23 -0
- package/dist/src/mcp/tools/memberships.d.ts +2 -0
- package/dist/src/mcp/tools/memberships.js +47 -0
- package/dist/src/mcp/tools/metadataTemplate.d.ts +2 -0
- package/dist/src/mcp/tools/metadataTemplate.js +42 -0
- package/dist/src/mcp/tools/org.d.ts +2 -0
- package/dist/src/mcp/tools/org.js +12 -0
- package/dist/src/mcp/tools/publishApp.d.ts +2 -0
- package/dist/src/mcp/tools/publishApp.js +39 -0
- package/dist/src/mcp/tools/roles.d.ts +2 -0
- package/dist/src/mcp/tools/roles.js +15 -0
- package/dist/src/mcp/tools/search.d.ts +2 -0
- package/dist/src/mcp/tools/search.js +16 -0
- package/dist/src/mcp/tools/sharedLinks.d.ts +2 -0
- package/dist/src/mcp/tools/sharedLinks.js +27 -0
- package/dist/src/mcp/tools/tags.d.ts +2 -0
- package/dist/src/mcp/tools/tags.js +20 -0
- package/dist/src/mcp/tools/themes.d.ts +2 -0
- package/dist/src/mcp/tools/themes.js +28 -0
- package/dist/src/mcp/tools/types.d.ts +15 -0
- package/dist/src/mcp/tools/types.js +1 -0
- package/dist/src/mcp/tools/users.d.ts +2 -0
- package/dist/src/mcp/tools/users.js +31 -0
- 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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
const html =
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
121
|
-
.option('--
|
|
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')
|
|
@@ -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 {
|
|
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
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
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,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,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,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,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,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,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,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,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
|
+
};
|