@pitcher/cli-scripts 3.1.1 → 3.3.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 (70) hide show
  1. package/README.md +39 -2
  2. package/dist/index.js +2 -0
  3. package/dist/src/commands/analytics.d.ts +77 -0
  4. package/dist/src/commands/analytics.js +309 -1
  5. package/dist/src/commands/dsrLanding.js +1 -1
  6. package/dist/src/commands/roleplay.d.ts +236 -0
  7. package/dist/src/commands/roleplay.js +702 -0
  8. package/dist/src/core/dsr-engagement.d.ts +152 -0
  9. package/dist/src/core/dsr-engagement.js +301 -0
  10. package/dist/src/core/dsr-visits.d.ts +50 -0
  11. package/dist/src/core/dsr-visits.js +191 -0
  12. package/dist/src/core/envelope.d.ts +1 -0
  13. package/dist/src/core/envelope.js +6 -1
  14. package/dist/src/core/errors.d.ts +3 -1
  15. package/dist/src/core/errors.js +2 -2
  16. package/dist/src/core/http.js +7 -5
  17. package/dist/src/core/ld.js +2 -1
  18. package/dist/src/core/me.d.ts +3 -0
  19. package/dist/src/core/me.js +10 -0
  20. package/dist/src/core/roleplay-stats.d.ts +120 -0
  21. package/dist/src/core/roleplay-stats.js +171 -0
  22. package/dist/src/mcp/apps/confirm/define.d.ts +66 -0
  23. package/dist/src/mcp/apps/confirm/define.js +208 -0
  24. package/dist/src/mcp/apps/confirm/hash.d.ts +4 -0
  25. package/dist/src/mcp/apps/confirm/hash.js +23 -0
  26. package/dist/src/mcp/apps/confirm/types.d.ts +96 -0
  27. package/dist/src/mcp/apps/confirm/types.js +15 -0
  28. package/dist/src/mcp/apps/contentPicker.d.ts +104 -0
  29. package/dist/src/mcp/apps/contentPicker.js +268 -0
  30. package/dist/src/mcp/apps/dsrEngagement.d.ts +194 -0
  31. package/dist/src/mcp/apps/dsrEngagement.js +737 -0
  32. package/dist/src/mcp/apps/registry.d.ts +14 -0
  33. package/dist/src/mcp/apps/registry.js +25 -0
  34. package/dist/src/mcp/apps/roleplayManager.d.ts +228 -0
  35. package/dist/src/mcp/apps/roleplayManager.js +606 -0
  36. package/dist/src/mcp/apps/roleplayReps.d.ts +79 -0
  37. package/dist/src/mcp/apps/roleplayReps.js +328 -0
  38. package/dist/src/mcp/apps/roleplayWrites.d.ts +6 -0
  39. package/dist/src/mcp/apps/roleplayWrites.js +509 -0
  40. package/dist/src/mcp/apps/text.d.ts +41 -0
  41. package/dist/src/mcp/apps/text.js +58 -0
  42. package/dist/src/mcp/apps/types.d.ts +23 -0
  43. package/dist/src/mcp/apps/types.js +12 -0
  44. package/dist/src/mcp/apps/writeConfirm.d.ts +3 -0
  45. package/dist/src/mcp/apps/writeConfirm.js +8 -0
  46. package/dist/src/mcp/groups.d.ts +42 -0
  47. package/dist/src/mcp/groups.js +89 -0
  48. package/dist/src/mcp/hosted.d.ts +36 -0
  49. package/dist/src/mcp/hosted.js +47 -1
  50. package/dist/src/mcp/index.d.ts +9 -3
  51. package/dist/src/mcp/index.js +9 -2
  52. package/dist/src/mcp/prompts/registry.js +8 -7
  53. package/dist/src/mcp/prompts/types.d.ts +3 -0
  54. package/dist/src/mcp/registry.d.ts +4 -0
  55. package/dist/src/mcp/registry.js +30 -23
  56. package/dist/src/mcp/renderPolicy.d.ts +2 -1
  57. package/dist/src/mcp/renderPolicy.js +10 -3
  58. package/dist/src/mcp/resources/analyticsCatalog.d.ts +1 -1
  59. package/dist/src/mcp/resources/analyticsCatalog.js +23 -5
  60. package/dist/src/mcp/resources/registry.js +2 -2
  61. package/dist/src/mcp/resources/types.d.ts +3 -0
  62. package/dist/src/mcp/result.d.ts +2 -1
  63. package/dist/src/mcp/result.js +8 -6
  64. package/dist/src/mcp/server.d.ts +62 -0
  65. package/dist/src/mcp/server.js +161 -10
  66. package/dist/src/mcp/tools/analytics.js +88 -3
  67. package/dist/src/mcp/tools/roleplay.d.ts +2 -0
  68. package/dist/src/mcp/tools/roleplay.js +135 -0
  69. package/dist/src/mcp/tools/types.d.ts +35 -0
  70. package/package.json +7 -1
@@ -0,0 +1,42 @@
1
+ /**
2
+ * PIT-10114: tool groups, the per-org policy and the access class of every tool.
3
+ *
4
+ * An org sets each group to off | read | read_write in `pitcher_organization.settings.mcp.groups`; `settings.mcp.access`
5
+ * stays the master switch and the ceiling (read_only caps every group at read). L&D is off unless the org turns it on.
6
+ * Inactive tools are absent from tools/list, and a call to one is refused and recorded.
7
+ */
8
+ import type { ToolDescriptor } from './tools/types.js';
9
+ export declare const TOOL_GROUPS: readonly ["content", "sharing", "analytics", "roleplay", "training_studio", "ld", "admin", "search", "developer"];
10
+ export type ToolGroup = (typeof TOOL_GROUPS)[number];
11
+ /** Policy class: 'write' = changes data OR exists only to lead to a change (PIT-10113 _propose and _commit). */
12
+ export type ToolAccess = 'read' | 'write';
13
+ export type GroupLevel = 'off' | 'read' | 'read_write';
14
+ export type ToolPolicy = Readonly<Record<ToolGroup, GroupLevel>>;
15
+ /**
16
+ * Groups an org may configure (developer is local-only). Django mirrors this list (`MCP_GROUPS` in
17
+ * ng-rest-api `pitcher/utils/validators.py`, which refuses unknown groups): add a group THERE FIRST.
18
+ */
19
+ export declare const CONFIGURABLE_GROUPS: readonly ToolGroup[];
20
+ export interface ResolvedPolicy {
21
+ policy: ToolPolicy;
22
+ /** Invalid or unknown keys ('groups' when malformed); logged once per fetch, never shown to users. */
23
+ warnings: string[];
24
+ }
25
+ export declare const OFF_POLICY: ToolPolicy;
26
+ /** `settings.mcp` → per-group levels. Fails closed: no valid `access`, malformed `groups`, an invalid level. */
27
+ export declare function resolveToolPolicy(mcp: unknown): ResolvedPolicy;
28
+ type AccessFields = Pick<ToolDescriptor, 'access' | 'readOnly' | 'write' | 'alwaysDestructive'>;
29
+ /** The policy class of a tool: explicit `access`, else the legacy read-only predicate (frozen for today's tools). */
30
+ export declare function accessOf(t: AccessFields): ToolAccess;
31
+ export type ToolVerdict = 'allowed' | 'GROUP_INACTIVE' | 'GROUP_READ_ONLY' | 'UNGROUPED';
32
+ export declare function toolVerdict(policy: ToolPolicy, t: AccessFields & Pick<ToolDescriptor, 'group'>): ToolVerdict;
33
+ /** A prompt drives a write tool unless flagged `readOnly`, so a group at `read` serves only the read-only ones. */
34
+ export declare function allowsPrompt(policy: ToolPolicy, p: {
35
+ group?: ToolGroup;
36
+ readOnly?: true;
37
+ }): boolean;
38
+ /** Hard rules on every registered descriptor: a group, and no write tool classed `read`. */
39
+ export declare function checkDescriptor(t: ToolDescriptor): ToolDescriptor;
40
+ /** Assign a whole module to a group; a descriptor that already names another group throws. */
41
+ export declare function inGroup(group: ToolGroup, tools: ToolDescriptor[]): ToolDescriptor[];
42
+ export {};
@@ -0,0 +1,89 @@
1
+ export const TOOL_GROUPS = [
2
+ 'content',
3
+ 'sharing',
4
+ 'analytics',
5
+ 'roleplay',
6
+ 'training_studio',
7
+ 'ld',
8
+ 'admin',
9
+ 'search',
10
+ 'developer',
11
+ ];
12
+ /**
13
+ * Groups an org may configure (developer is local-only). Django mirrors this list (`MCP_GROUPS` in
14
+ * ng-rest-api `pitcher/utils/validators.py`, which refuses unknown groups): add a group THERE FIRST.
15
+ */
16
+ export const CONFIGURABLE_GROUPS = TOOL_GROUPS.filter((g) => g !== 'developer');
17
+ const DEFAULT_OFF = new Set(['ld']); // N1 ruling, 2026-11-10
18
+ const RANK = { off: 0, read: 1, read_write: 2 };
19
+ const LEVELS = new Set(['off', 'read', 'read_write']);
20
+ /** The keys `settings.mcp` may hold; mirrors `MCP_KEYS` in ng-rest-api `pitcher/utils/validators.py`. */
21
+ const MCP_KEYS = new Set(['access', 'groups']);
22
+ export const OFF_POLICY = Object.freeze(Object.fromEntries(TOOL_GROUPS.map((g) => [g, 'off'])));
23
+ /** `settings.mcp` → per-group levels. Fails closed: no valid `access`, malformed `groups`, an invalid level. */
24
+ export function resolveToolPolicy(mcp) {
25
+ const raw = (mcp && typeof mcp === 'object' && !Array.isArray(mcp) ? mcp : {});
26
+ if (raw.access !== 'full' && raw.access !== 'read_only')
27
+ return { policy: OFF_POLICY, warnings: [] };
28
+ const ceiling = raw.access === 'full' ? 'read_write' : 'read';
29
+ const g0 = raw.groups ?? {};
30
+ if (typeof g0 !== 'object' || Array.isArray(g0))
31
+ return { policy: OFF_POLICY, warnings: ['groups'] };
32
+ const groups = g0;
33
+ const warnings = [
34
+ // A top-level typo (`group`, `Groups`) leaves `groups` absent, so every group takes its default. Django refuses
35
+ // unknown keys at save (ng-rest-api#1809, `MCP_KEYS`); report any that reach us anyway, as `mcp.<key>`.
36
+ ...Object.keys(raw).filter((k) => !MCP_KEYS.has(k)).map((k) => `mcp.${k}`),
37
+ ...Object.keys(groups).filter((k) => !CONFIGURABLE_GROUPS.includes(k)),
38
+ ];
39
+ const policy = { ...OFF_POLICY };
40
+ for (const g of CONFIGURABLE_GROUPS) {
41
+ const v = groups[g];
42
+ let requested;
43
+ if (v === undefined)
44
+ requested = DEFAULT_OFF.has(g) ? 'off' : ceiling;
45
+ else if (typeof v === 'string' && LEVELS.has(v))
46
+ requested = v;
47
+ else {
48
+ requested = 'off';
49
+ warnings.push(g);
50
+ }
51
+ policy[g] = RANK[requested] <= RANK[ceiling] ? requested : ceiling;
52
+ }
53
+ return { policy: Object.freeze(policy), warnings };
54
+ }
55
+ /** The policy class of a tool: explicit `access`, else the legacy read-only predicate (frozen for today's tools). */
56
+ export function accessOf(t) {
57
+ return t.access ?? (t.readOnly && !t.write && !t.alwaysDestructive ? 'read' : 'write');
58
+ }
59
+ export function toolVerdict(policy, t) {
60
+ if (!t.group)
61
+ return 'UNGROUPED'; // fail closed
62
+ const level = policy[t.group];
63
+ if (level === 'off')
64
+ return 'GROUP_INACTIVE';
65
+ return accessOf(t) === 'read' || level === 'read_write' ? 'allowed' : 'GROUP_READ_ONLY';
66
+ }
67
+ /** A prompt drives a write tool unless flagged `readOnly`, so a group at `read` serves only the read-only ones. */
68
+ export function allowsPrompt(policy, p) {
69
+ if (!p.group)
70
+ return false;
71
+ const level = policy[p.group];
72
+ return level === 'read_write' || (level === 'read' && p.readOnly === true);
73
+ }
74
+ /** Hard rules on every registered descriptor: a group, and no write tool classed `read`. */
75
+ export function checkDescriptor(t) {
76
+ if (!t.group)
77
+ throw new Error(`${t.name}: no tool group`);
78
+ if ((t.write || t.alwaysDestructive) && t.access === 'read')
79
+ throw new Error(`${t.name}: a write tool cannot be access 'read'`);
80
+ return t;
81
+ }
82
+ /** Assign a whole module to a group; a descriptor that already names another group throws. */
83
+ export function inGroup(group, tools) {
84
+ return tools.map((t) => {
85
+ if (t.group && t.group !== group)
86
+ throw new Error(`${t.name}: group ${t.group} conflicts with ${group}`);
87
+ return checkDescriptor({ ...t, group });
88
+ });
89
+ }
@@ -1,3 +1,6 @@
1
+ import type { AppViewDescriptor } from './apps/types.js';
2
+ import { type ToolPolicy } from './groups.js';
3
+ import type { ResourceDescriptor } from './resources/types.js';
1
4
  import type { PromptDescriptor } from './prompts/types.js';
2
5
  import type { BuildServerOptions } from './server.js';
3
6
  import type { ToolDescriptor } from './tools/types.js';
@@ -32,5 +35,38 @@ export declare const hostedTools: ToolDescriptor[];
32
35
  * A write tool is never registered, so calling one is "unknown tool", not a refused write.
33
36
  */
34
37
  export declare const hostedReadOnlyTools: ToolDescriptor[];
38
+ /** MCP Apps tools (PIT-10066), hosted-shaped: the same file/id runtime guards as every hosted tool. */
39
+ export declare const hostedAppTools: ToolDescriptor[];
40
+ /** A read-only server's app tool: read class, and never a PIT-10113 confirm tool, whatever its `access` says. */
41
+ export declare const isHostedReadOnlyAppTool: (t: ToolDescriptor) => boolean;
42
+ /** The app tools a read-only server serves (same rule as hostedReadOnlyTools). */
43
+ export declare const hostedReadOnlyAppTools: ToolDescriptor[];
35
44
  /** The prompts a read-only server serves: only those flagged `readOnly` (the rest drive a write tool). */
36
45
  export declare const readOnlyPrompts: PromptDescriptor[];
46
+ /** The CLI-backed hosted tools this policy serves. */
47
+ export declare const hostedToolsFor: (p: ToolPolicy) => ToolDescriptor[];
48
+ /** What decides app-tool registration besides the policy: the enabled views, and whether a PIT-10113 signer exists. */
49
+ export interface HostedFeatures {
50
+ views: AppViewDescriptor[];
51
+ proposals: boolean;
52
+ }
53
+ /** The app tools this policy serves: allowed, their view enabled, and a confirm tool only with a proposal signer. */
54
+ export declare const hostedAppToolsFor: (p: ToolPolicy, f: HostedFeatures) => ToolDescriptor[];
55
+ /**
56
+ * Views worth serving: only those with at least one surviving app tool. App tools are filtered by their own group
57
+ * first, so a view whose group is off (analytics off hides dsr-engagement) is never served.
58
+ */
59
+ export declare const viewsFor: (p: ToolPolicy, f: HostedFeatures) => AppViewDescriptor[];
60
+ /** Exactly the names buildServer registers for this policy and these features (one filter, used by build and recording). */
61
+ export declare const hostedRegisteredNames: (p: ToolPolicy, f: HostedFeatures) => Set<string>;
62
+ export declare const promptsFor: (p: ToolPolicy) => PromptDescriptor[];
63
+ export declare const resourcesFor: (p: ToolPolicy) => ResourceDescriptor[];
64
+ /** The HOSTED descriptor (hosted inputShape, never a LOCAL_ONLY name) by name. */
65
+ export declare function hostedToolByName(name: string): ToolDescriptor | undefined;
66
+ /**
67
+ * Whether `args` pass the hosted tool's input schema: the same `inputShape` object `registerTool` receives, so the
68
+ * mcp-server pre-inspection sees exactly the calls the SDK rejects before the callback (recorded as INVALID_ARGS).
69
+ */
70
+ export declare function hostedArgsValid(t: ToolDescriptor, args: unknown): boolean;
71
+ /** On = at least one hosted tool survives; an empty group (training_studio) does not keep the connector on. */
72
+ export declare const policyIsOn: (p: ToolPolicy) => boolean;
@@ -1,6 +1,10 @@
1
+ import { z } from 'zod';
1
2
  import { fail } from '../core/envelope.js';
2
3
  import { createCliError } from '../core/errors.js';
4
+ import { allAppTools } from './apps/registry.js';
5
+ import { accessOf, allowsPrompt, toolVerdict } from './groups.js';
3
6
  import { allPrompts } from './prompts/registry.js';
7
+ import { allResources } from './resources/registry.js';
4
8
  import { allTools } from './registry.js';
5
9
  /**
6
10
  * The tool set served by the hosted (remote, OAuth) MCP server — PIT-8912.
@@ -110,6 +114,48 @@ export const hostedTools = allTools.filter((t) => !LOCAL_ONLY.has(t.name)).map(t
110
114
  * Read-only mode (the org's `settings.mcp.access` is `read_only`, PIT-9768): only the hosted tools that read.
111
115
  * A write tool is never registered, so calling one is "unknown tool", not a refused write.
112
116
  */
113
- export const hostedReadOnlyTools = hostedTools.filter((t) => t.readOnly && !t.write && !t.alwaysDestructive);
117
+ export const hostedReadOnlyTools = hostedTools.filter((t) => accessOf(t) === 'read');
118
+ /** MCP Apps tools (PIT-10066), hosted-shaped: the same file/id runtime guards as every hosted tool. */
119
+ export const hostedAppTools = allAppTools.map(toHosted);
120
+ /** A read-only server's app tool: read class, and never a PIT-10113 confirm tool, whatever its `access` says. */
121
+ export const isHostedReadOnlyAppTool = (t) => accessOf(t) === 'read' && !t.confirm;
122
+ /** The app tools a read-only server serves (same rule as hostedReadOnlyTools). */
123
+ export const hostedReadOnlyAppTools = hostedAppTools.filter(isHostedReadOnlyAppTool);
114
124
  /** The prompts a read-only server serves: only those flagged `readOnly` (the rest drive a write tool). */
115
125
  export const readOnlyPrompts = allPrompts.filter((p) => p.readOnly === true);
126
+ // ---------------------------------------------------------------------------------------------------------------------
127
+ // PIT-10114: per-org tool groups. Every hosted list below is derived from the org's policy (`resolveToolPolicy`).
128
+ const allowed = (p) => (t) => toolVerdict(p, t) === 'allowed';
129
+ /** The CLI-backed hosted tools this policy serves. */
130
+ export const hostedToolsFor = (p) => hostedTools.filter(allowed(p));
131
+ /** The app tools this policy serves: allowed, their view enabled, and a confirm tool only with a proposal signer. */
132
+ export const hostedAppToolsFor = (p, f) => {
133
+ const on = new Set(f.views.map((v) => v.name));
134
+ return hostedAppTools.filter((t) => allowed(p)(t) && (!t.ui || on.has(t.ui.view)) && (!t.confirm || f.proposals));
135
+ };
136
+ /**
137
+ * Views worth serving: only those with at least one surviving app tool. App tools are filtered by their own group
138
+ * first, so a view whose group is off (analytics off hides dsr-engagement) is never served.
139
+ */
140
+ export const viewsFor = (p, f) => {
141
+ const used = new Set(hostedAppToolsFor(p, f).map((t) => t.ui.view));
142
+ return f.views.filter((v) => used.has(v.name));
143
+ };
144
+ /** Exactly the names buildServer registers for this policy and these features (one filter, used by build and recording). */
145
+ export const hostedRegisteredNames = (p, f) => new Set([...hostedToolsFor(p), ...hostedAppToolsFor(p, f)].map((t) => t.name));
146
+ export const promptsFor = (p) => allPrompts.filter((x) => allowsPrompt(p, x));
147
+ export const resourcesFor = (p) => allResources.filter((r) => !r.group || p[r.group] !== 'off');
148
+ const HOSTED_BY_NAME = new Map([...hostedTools, ...hostedAppTools].map((t) => [t.name, t]));
149
+ /** The HOSTED descriptor (hosted inputShape, never a LOCAL_ONLY name) by name. */
150
+ export function hostedToolByName(name) {
151
+ return HOSTED_BY_NAME.get(name);
152
+ }
153
+ /**
154
+ * Whether `args` pass the hosted tool's input schema: the same `inputShape` object `registerTool` receives, so the
155
+ * mcp-server pre-inspection sees exactly the calls the SDK rejects before the callback (recorded as INVALID_ARGS).
156
+ */
157
+ export function hostedArgsValid(t, args) {
158
+ return z.object(t.inputShape).safeParse(args ?? {}).success;
159
+ }
160
+ /** On = at least one hosted tool survives; an empty group (training_studio) does not keep the connector on. */
161
+ export const policyIsOn = (p) => hostedToolsFor(p).length > 0;
@@ -1,7 +1,13 @@
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';
1
+ export { buildServer, PITCHER_ICON, CLI_SCRIPTS_VERSION, toolCallEvent, type McpRunContext, type BuildServerOptions, type ToolCallEvent, type ToolCallSink, } from './server.js';
2
+ export { TOOL_GROUPS, CONFIGURABLE_GROUPS, OFF_POLICY, resolveToolPolicy, toolVerdict, accessOf, allowsPrompt, type ToolGroup, type ToolAccess, type GroupLevel, type ToolPolicy, type ResolvedPolicy, type ToolVerdict, } from './groups.js';
3
+ export { hostedTools, hostedReadOnlyTools, readOnlyPrompts, LOCAL_ONLY, FILE_KEY, ID_KEY, HOSTED_BUILD_OPTIONS, hostedAppTools, hostedReadOnlyAppTools, hostedToolsFor, hostedAppToolsFor, viewsFor, hostedRegisteredNames, promptsFor, resourcesFor, hostedToolByName, hostedArgsValid, policyIsOn, type HostedFeatures, } from './hosted.js';
4
+ export { allAppViews, allAppTools } from './apps/registry.js';
5
+ export { PROPOSAL_TTL_S, PROPOSAL_MAX_BYTES, PROPOSAL_TEXT_MAX_BYTES, TEXT_HOST_MODE, type ProposalClaims, type ProposalSigner, type HostClient, type Proposal, type CommitResult, } from './apps/confirm/types.js';
6
+ export { APPS_RENDER_POLICY } from './renderPolicy.js';
7
+ export { appViewUri, type AppViewDescriptor, type AppCsp } from './apps/types.js';
3
8
  export { allTools } from './registry.js';
4
9
  export { allPrompts } from './prompts/registry.js';
5
10
  export { allResources } from './resources/registry.js';
6
- export type { ToolDescriptor } from './tools/types.js';
11
+ export type { ToolDescriptor, ToolUi, AppInjected } from './tools/types.js';
7
12
  export type { Credential } from '../core/credential.js';
13
+ export { createCliError, isCliError, type CliError } from '../core/errors.js';
@@ -1,6 +1,13 @@
1
1
  // Public entry `@pitcher/cli-scripts/mcp` — what the hosted MCP server (web/packages/mcp-server) consumes.
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';
2
+ export { buildServer, PITCHER_ICON, CLI_SCRIPTS_VERSION, toolCallEvent, } from './server.js';
3
+ export { TOOL_GROUPS, CONFIGURABLE_GROUPS, OFF_POLICY, resolveToolPolicy, toolVerdict, accessOf, allowsPrompt, } from './groups.js';
4
+ export { hostedTools, hostedReadOnlyTools, readOnlyPrompts, LOCAL_ONLY, FILE_KEY, ID_KEY, HOSTED_BUILD_OPTIONS, hostedAppTools, hostedReadOnlyAppTools, hostedToolsFor, hostedAppToolsFor, viewsFor, hostedRegisteredNames, promptsFor, resourcesFor, hostedToolByName, hostedArgsValid, policyIsOn, } from './hosted.js';
5
+ export { allAppViews, allAppTools } from './apps/registry.js';
6
+ export { PROPOSAL_TTL_S, PROPOSAL_MAX_BYTES, PROPOSAL_TEXT_MAX_BYTES, TEXT_HOST_MODE, } from './apps/confirm/types.js';
7
+ export { APPS_RENDER_POLICY } from './renderPolicy.js';
8
+ export { appViewUri } from './apps/types.js';
4
9
  export { allTools } from './registry.js';
5
10
  export { allPrompts } from './prompts/registry.js';
6
11
  export { allResources } from './resources/registry.js';
12
+ // PIT-10113: the proposal signer (mcp-server) throws CliErrors so a commit maps them to the right code
13
+ export { createCliError, isCliError } from '../core/errors.js';
@@ -4,12 +4,13 @@ import { dsrPrompts } from './dsr.js';
4
4
  import { announcementsPrompts } from './announcements.js';
5
5
  import { ldCanvasCoursePrompts } from './ldCanvasCourse.js';
6
6
  import { analyticsPrompts } from './analytics.js';
7
- // Every recipe prompt the server can expose. Mirrors mcp/registry.ts (allTools).
7
+ const withGroup = (group, prompts) => prompts.map((p) => ({ ...p, group }));
8
+ // Every recipe prompt the server can expose, each in the group of the tools it drives (PIT-10114). Mirrors mcp/registry.ts.
8
9
  export const allPrompts = [
9
- ...emailTemplatePrompts,
10
- ...dsrLandingPrompts,
11
- ...dsrPrompts,
12
- ...announcementsPrompts,
13
- ...ldCanvasCoursePrompts,
14
- ...analyticsPrompts,
10
+ ...withGroup('sharing', emailTemplatePrompts),
11
+ ...withGroup('sharing', dsrLandingPrompts),
12
+ ...withGroup('sharing', dsrPrompts),
13
+ ...withGroup('content', announcementsPrompts),
14
+ ...withGroup('ld', ldCanvasCoursePrompts),
15
+ ...withGroup('analytics', analyticsPrompts),
15
16
  ];
@@ -1,5 +1,6 @@
1
1
  import type { ZodOptional, ZodString } from 'zod';
2
2
  import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js';
3
+ import type { ToolGroup } from '../groups.js';
3
4
  export interface PromptDescriptor {
4
5
  name: string;
5
6
  title: string;
@@ -7,4 +8,6 @@ export interface PromptDescriptor {
7
8
  argsSchema: Record<string, ZodOptional<ZodString>>;
8
9
  build: (args: Record<string, string | undefined>) => PromptMessage[];
9
10
  readOnly?: true;
11
+ /** PIT-10114: served when its group is read_write, or at read when `readOnly`. Set in prompts/registry.ts. */
12
+ group?: ToolGroup;
10
13
  }
@@ -1,2 +1,6 @@
1
1
  import type { ToolDescriptor } from './tools/types.js';
2
+ /**
3
+ * Every CLI-backed tool, each module assigned whole to one tool group (PIT-10114, spec "Group Taxonomy"). `inGroup`
4
+ * throws when a descriptor names another group or a write tool is classed `read` (checkDescriptor).
5
+ */
2
6
  export declare const allTools: ToolDescriptor[];
@@ -1,3 +1,4 @@
1
+ import { inGroup } from './groups.js';
1
2
  import { analyticsTools } from './tools/analytics.js';
2
3
  import { announcementsTools } from './tools/announcements.js';
3
4
  import { canvasesTools } from './tools/canvases.js';
@@ -15,34 +16,40 @@ import { metadataTemplateTools } from './tools/metadataTemplate.js';
15
16
  import { ldTools } from './tools/ld.js';
16
17
  import { orgTools } from './tools/org.js';
17
18
  import { publishAppTools } from './tools/publishApp.js';
19
+ import { roleplayTools } from './tools/roleplay.js';
18
20
  import { rolesTools } from './tools/roles.js';
19
21
  import { searchTools } from './tools/search.js';
20
22
  import { sharedLinksTools } from './tools/sharedLinks.js';
21
23
  import { tagsTools } from './tools/tags.js';
22
24
  import { themesTools } from './tools/themes.js';
23
25
  import { usersTools } from './tools/users.js';
26
+ /**
27
+ * Every CLI-backed tool, each module assigned whole to one tool group (PIT-10114, spec "Group Taxonomy"). `inGroup`
28
+ * throws when a descriptor names another group or a write tool is classed `read` (checkDescriptor).
29
+ */
24
30
  export const allTools = [
25
- ...canvasesTools,
26
- ...filesTools,
27
- ...foldersTools,
28
- ...searchTools,
29
- ...instancesTools,
30
- ...sharedLinksTools,
31
- ...tagsTools,
32
- ...announcementsTools,
33
- ...dsrLandingTools,
34
- ...dsrTools,
35
- ...emailTemplateTools,
36
- ...themesTools,
37
- ...usersTools,
38
- ...invitationsTools,
39
- ...rolesTools,
40
- ...orgTools,
41
- ...analyticsTools,
42
- ...membershipsTools,
43
- ...groupsTools,
44
- ...metadataTemplateTools,
45
- ...ldTools,
46
- ...devSyncTools,
47
- ...publishAppTools,
31
+ ...inGroup('content', canvasesTools),
32
+ ...inGroup('content', filesTools),
33
+ ...inGroup('content', foldersTools),
34
+ ...inGroup('search', searchTools),
35
+ ...inGroup('search', instancesTools),
36
+ ...inGroup('sharing', sharedLinksTools),
37
+ ...inGroup('content', tagsTools),
38
+ ...inGroup('content', announcementsTools),
39
+ ...inGroup('sharing', dsrLandingTools),
40
+ ...inGroup('sharing', dsrTools),
41
+ ...inGroup('sharing', emailTemplateTools),
42
+ ...inGroup('content', themesTools),
43
+ ...inGroup('admin', usersTools),
44
+ ...inGroup('admin', invitationsTools),
45
+ ...inGroup('admin', rolesTools),
46
+ ...inGroup('search', orgTools),
47
+ ...inGroup('analytics', analyticsTools),
48
+ ...inGroup('roleplay', roleplayTools),
49
+ ...inGroup('admin', membershipsTools),
50
+ ...inGroup('admin', groupsTools),
51
+ ...inGroup('content', metadataTemplateTools),
52
+ ...inGroup('ld', ldTools),
53
+ ...inGroup('developer', devSyncTools),
54
+ ...inGroup('developer', publishAppTools),
48
55
  ];
@@ -1 +1,2 @@
1
- export declare const ANALYTICS_RENDER_POLICY = "When answering Pitcher content-analytics questions (the pitcher_analytics_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.\n\nRendering tiers (evaluate top-down, stop at the first match):\n- A single fact / yes-no / \"who/what/how many is the single X\" (e.g. \"which canvas did I view?\", \"how many viewers?\") -> 1-2 sentences with the number inline. No table, no chart.\n- A ranking request (\"top\", \"most\", \"best\", \"rank\", or \"list the highest/lowest ...\") about content, files, users, or canvases -> a markdown TABLE of the ranked rows (default the top 5-8); columns = only the metric(s) asked for. Treat \"what's my top content?\" as a ranking (a table), NOT a single-item sentence. Lead with a one-line takeaway, then the table.\n- \"over time\" / \"trend\" / \"by week/month\" -> ONE line/area chart artifact (this rule wins over the generic comparison rule below for time series).\n- An explicit visualization request (\"show a chart\", \"chart\", \"visualize\", \"graph\", \"plot\") OR a comparison across a non-time dimension -> ONE focused chart as a single self-contained HTML artifact, only the asked-for series.\n- A broad overview (\"how's everything / my content / adoption doing\", \"give me the picture\", \"dashboard\", QBR) -> a multi-panel dashboard artifact. This is the ceiling \u2014 use it rarely, only for genuinely broad asks.\n\nGuardrails (every tier):\n1. Words first \u2014 even with a chart, lead with a one-sentence answer to the literal question.\n2. Mask names, emails, and user IDs by default (User A/B/...). Reveal an identity only when the analytics tool actually returned it for the authenticated role (the verbs are role-gated); never infer authorization from the user's wording.\n3. If data is empty or trivially small, say so in prose \u2014 do not render an empty chart.\n4. If results are capped/truncated, state the cap (\"top 15 of 118\").\n5. Never show a \"vs prior period\" delta unless the data actually contains prior-period numbers.\n6. Cite the source verb (\"via analytics popularity\").\n\nDo not jump a tier without its trigger. The dashboard is opt-in by intent, never a default.";
1
+ export declare const ANALYTICS_RENDER_POLICY = "When answering Pitcher content-analytics or AI Roleplay questions (the pitcher_analytics_* and pitcher_roleplay_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.\n\nRendering tiers (evaluate top-down, stop at the first match):\n- A single fact / yes-no / \"who/what/how many is the single X\" (e.g. \"which canvas did I view?\", \"how many viewers?\") -> 1-2 sentences with the number inline. No table, no chart.\n- A ranking request (\"top\", \"most\", \"best\", \"rank\", or \"list the highest/lowest ...\") about content, files, users, or canvases -> a markdown TABLE of the ranked rows (default the top 5-8); columns = only the metric(s) asked for. Treat \"what's my top content?\" as a ranking (a table), NOT a single-item sentence. Lead with a one-line takeaway, then the table.\n- \"over time\" / \"trend\" / \"by week/month\" -> ONE line/area chart artifact (this rule wins over the generic comparison rule below for time series).\n- An explicit visualization request (\"show a chart\", \"chart\", \"visualize\", \"graph\", \"plot\") OR a comparison across a non-time dimension -> ONE focused chart as a single self-contained HTML artifact, only the asked-for series.\n- A broad overview (\"how's everything / my content / adoption doing\", \"give me the picture\", \"dashboard\", QBR) -> a multi-panel dashboard artifact. This is the ceiling \u2014 use it rarely, only for genuinely broad asks.\n\nGuardrails (every tier):\n1. Words first \u2014 even with a chart, lead with a one-sentence answer to the literal question.\n2. Mask names, emails, and user IDs by default (User A/B/...). Reveal an identity only when the analytics tool actually returned it for the authenticated role (the verbs are role-gated); never infer authorization from the user's wording.\n3. If data is empty or trivially small, say so in prose \u2014 do not render an empty chart.\n4. If results are capped/truncated, state the cap (\"top 15 of 118\").\n5. Never show a \"vs prior period\" delta unless the data actually contains prior-period numbers.\n6. Cite the source verb (\"via analytics popularity\").\n7. AI Roleplay: a session with no score (null) is unscored, never a 0; never fold it into an average, a pass rate or a ranking.\n\nDo not jump a tier without its trigger. The dashboard is opt-in by intent, never a default.";
2
+ export declare const APPS_RENDER_POLICY = "When your host actually renders a Pitcher tool result in its own interactive view (MCP Apps: a Pitcher view), answer in one sentence and do not redraw the same data as a table, chart or artifact. If your host does not render the view, the text result is all the user gets: list the items for the user.\n\nConfirmed writes (tools ending in _propose): a propose call saves nothing. If your host shows the Pitcher confirm view, the user confirms or cancels there: say one sentence and do nothing else. Never call a _commit tool unless the proposal text tells you how and the user explicitly confirmed in this conversation; never change its arguments; never retry a commit the user cancelled.";
@@ -1,9 +1,10 @@
1
1
  // Connector-level MCP instructions surfaced to the host model (Claude Desktop, Cursor) via
2
2
  // InitializeResult.instructions. Governs how analytics answers are RENDERED so a natural-language
3
3
  // question gets a right-sized reply (a sentence, a table, one chart, or — rarely — a dashboard)
4
- // instead of a rigid full dashboard on every question. This constant is the single source of truth;
5
- // the Docs Portal spec (Product/Specs/2026-07-24-analytics-answer-rendering-policy.md) mirrors it.
6
- export const ANALYTICS_RENDER_POLICY = `When answering Pitcher content-analytics questions (the pitcher_analytics_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.
4
+ // instead of a rigid full dashboard on every question. This constant is the only copy of the policy:
5
+ // edit it here. The Docs Portal spec (Product/Specs/2026-07-24-analytics-answer-rendering-policy.md)
6
+ // points here and keeps only the background and decisions.
7
+ export const ANALYTICS_RENDER_POLICY = `When answering Pitcher content-analytics or AI Roleplay questions (the pitcher_analytics_* and pitcher_roleplay_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.
7
8
 
8
9
  Rendering tiers (evaluate top-down, stop at the first match):
9
10
  - A single fact / yes-no / "who/what/how many is the single X" (e.g. "which canvas did I view?", "how many viewers?") -> 1-2 sentences with the number inline. No table, no chart.
@@ -19,5 +20,11 @@ Guardrails (every tier):
19
20
  4. If results are capped/truncated, state the cap ("top 15 of 118").
20
21
  5. Never show a "vs prior period" delta unless the data actually contains prior-period numbers.
21
22
  6. Cite the source verb ("via analytics popularity").
23
+ 7. AI Roleplay: a session with no score (null) is unscored, never a 0; never fold it into an average, a pass rate or a ranking.
22
24
 
23
25
  Do not jump a tier without its trigger. The dashboard is opt-in by intent, never a default.`;
26
+ // MCP Apps (PIT-10066): appended to the server instructions ONLY when at least one view is enabled, so a
27
+ // server without apps keeps exactly ANALYTICS_RENDER_POLICY. The confirmed-writes paragraph is PIT-10113's (spec 8.2).
28
+ export const APPS_RENDER_POLICY = `When your host actually renders a Pitcher tool result in its own interactive view (MCP Apps: a Pitcher view), answer in one sentence and do not redraw the same data as a table, chart or artifact. If your host does not render the view, the text result is all the user gets: list the items for the user.
29
+
30
+ Confirmed writes (tools ending in _propose): a propose call saves nothing. If your host shows the Pitcher confirm view, the user confirms or cancels there: say one sentence and do nothing else. Never call a _commit tool unless the proposal text tells you how and the user explicitly confirmed in this conversation; never change its arguments; never retry a commit the user cancelled.`;
@@ -1,3 +1,3 @@
1
1
  import type { ResourceDescriptor } from './types.js';
2
- export declare const ANALYTICS_CATALOG = "# Pitcher analytics catalog\n\nTen read-only verbs, each an MCP tool named `pitcher_analytics_<verb>`. Pick the verb that matches the\nquestion, then call its tool. All are read-only; org scope + auth come from the server's API key (never a\ntool input) \u2014 you only pass `instanceId` (and, where noted, `canvasId`/`userEmail`).\n\n## Key tiers\n- **org-admin key required:** org, popularity, users, files, canvases\n- **instance-admin or org-admin:** instance\n- **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions\n\n## Time model\n- **Rollup (no window):** org, instance \u2014 lifetime/current totals.\n- **Date-range, 30-day default:** kpi (activity types need dates), engagement, popularity, `users` with\n `stats: true`, files, canvases \u2014 omit dates to get the last 30 days. (`users` without `stats` is a\n full roster with no window.)\n- **Date-range, server default window:** viewers, sessions \u2014 omit dates and the service picks the window.\n\n## Verbs\n\n| Verb (tool) | Returns | Time | Key | Example question |\n|---|---|---|---|---|\n| `org` | Organization-wide rollup totals (Django) | rollup | org-admin | \"How is the whole org doing overall?\" |\n| `instance` | Single-instance rollup totals (Django) | rollup | instance-admin | \"Totals for this one workspace?\" |\n| `kpi` | KPI metrics for an instance (members, active users, \u2026) | date-range | member | \"How many active users last month?\" |\n| `engagement` | Per-DSR/content engagement rows (capped list + summary) | date-range | member | \"Which content is being engaged with?\" |\n| `popularity` | Most-viewed canvases, ranked (view-analysis) | date-range | org-admin | \"Top 10 most-viewed decks this quarter?\" |\n| `users` | User roster, or active-user stats with `stats: true` | roster: none / stats: date-range | org-admin | \"Who are my active users this month?\" |\n| `files` | File usage analytics (views/opens/engagement per file) | date-range | org-admin | \"Which files get opened the most?\" |\n| `canvases` | Per-section canvas engagement list | date-range | org-admin | \"How are my canvas sections performing?\" |\n| `viewers` | Who viewed a specific DSR canvas (per-viewer engagement) | date-range | member | \"Who viewed this DSR, and how much?\" |\n| `sessions` | One viewer's session event timeline for a DSR canvas (raw) | date-range | member | \"What did this viewer do inside the DSR?\" |\n\n## Shaping\nRow-heavy verbs (engagement, popularity, users, files, canvases, viewers) return a capped envelope:\n`summary` + `rows` (default 20, max 100 \u2014 raise with `limit`) + `total_count`/`returned`/`has_more`,\nand a `truncated` note when more rows exist. `kpi` and `sessions` return the raw payload (a KPI scalar set\nand an uncapped event timeline, respectively) \u2014 never truncated.\n\n## DSR drill-down\nFor \"who viewed X and what did they do\": call `viewers` (with `canvasId`) to list viewers, then `sessions`\n(with the same `canvasId` + the viewer's `userEmail`) to see that viewer's event timeline.\nA session is one `Entered` of the queried id itself (a canvas or a section), paired by `event_pair_id`; an\n`Entered` with an empty `event_pair_id` counts as one session on its own.\n`session_count` is counted the same way as the overview's `total_sessions`. `canvases` lists sections, and their\nids are valid `canvasId` inputs for `viewers` and `sessions`.\n\n## Notes\n- `popularity` maps to the canvases view-analysis endpoint (there is no separate popularity route).\n- Results can include user identities/emails \u2014 treat as customer data and handle per policy.\n";
2
+ export declare const ANALYTICS_CATALOG = "# Pitcher analytics catalog\n\nFourteen read-only verbs, each an MCP tool named `pitcher_analytics_<verb>`. Pick the verb that matches the\nquestion, then call its tool. All are read-only; org scope + auth come from the server's API key (never a\ntool input) \u2014 you only pass `instanceId` (and, where noted, `canvasId`/`userEmail`).\n\n## Key tiers\n- **org-admin key required:** org, popularity, users, files, canvases\n- **instance-admin or org-admin:** instance\n- **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions, viewer_visits\n- **instance editor, instance admin or org admin:** file_users, file_pages, canvas_users\n\n## Time model\n- **Rollup (no window):** org, instance \u2014 lifetime/current totals.\n- **Date-range, 30-day default:** kpi (activity types need dates), engagement, popularity, `users` with\n `stats: true`, files, canvases \u2014 omit dates to get the last 30 days. (`users` without `stats` is a\n full roster with no window.) Also file_users, file_pages, canvas_users.\n- **Date-range, server default window:** viewers, sessions, viewer_visits \u2014 omit dates and the service picks the window.\n\n## Verbs\n\n| Verb (tool) | Returns | Time | Key | Example question |\n|---|---|---|---|---|\n| `org` | Organization-wide rollup totals (Django) | rollup | org-admin | \"How is the whole org doing overall?\" |\n| `instance` | Single-instance rollup totals (Django) | rollup | instance-admin | \"Totals for this one workspace?\" |\n| `kpi` | KPI metrics for an instance (members, active users, \u2026) | date-range | member | \"How many active users last month?\" |\n| `engagement` | Per-DSR/content engagement rows (capped list + summary) | date-range | member | \"Which content is being engaged with?\" |\n| `popularity` | Most-viewed canvases, ranked (view-analysis) | date-range | org-admin | \"Top 10 most-viewed decks this quarter?\" |\n| `users` | User roster, or active-user stats with `stats: true` | roster: none / stats: date-range | org-admin | \"Who are my active users this month?\" |\n| `files` | File usage analytics (views/opens/engagement per file) | date-range | org-admin | \"Which files get opened the most?\" |\n| `canvases` | Per-section canvas engagement list | date-range | org-admin | \"How are my canvas sections performing?\" |\n| `viewers` | Who viewed a specific DSR canvas (per-viewer engagement) | date-range | member | \"Who viewed this DSR, and how much?\" |\n| `sessions` | One viewer's session event timeline for a DSR canvas (raw) | date-range | member | \"What did this viewer do inside the DSR?\" |\n| `file_users` | Who opened ONE file: opens, seconds, days active per user | date-range | editor | \"Who opened the pricing PDF, and for how long?\" |\n| `file_pages` | Time per page of ONE file (one user with a single `userEmails`) | date-range | editor | \"Which pages of this file do reps spend time on?\" |\n| `canvas_users` | Per user and canvas: visits, seconds; with `canvasId`, seconds per page (section) | date-range | editor | \"Which reps presented this deck, and which slides?\" |\n| `viewer_visits` | Per DSR viewer and visit: sections, files and pages with seconds; `format: csv` to export | date-range | member | \"Export who looked at which pages of this DSR\" |\n\n## Shaping\nRow-heavy verbs (engagement, popularity, users, files, canvases, viewers) return a capped envelope:\n`summary` + `rows` (default 20, max 100 \u2014 raise with `limit`) + `total_count`/`returned`/`has_more`,\nand a `truncated` note when more rows exist. `kpi` and `sessions` return the raw payload (a KPI scalar set\nand an uncapped event timeline, respectively) \u2014 never truncated.\n\n## DSR drill-down\nFor \"who viewed X and what did they do\": call `viewers` (with `canvasId`) to list viewers, then `sessions`\n(with the same `canvasId` + the viewer's `userEmail`) to see that viewer's event timeline.\nA session is one `Entered` of the queried id itself (a canvas or a section), paired by `event_pair_id`; an\n`Entered` with an empty `event_pair_id` counts as one session on its own.\n`session_count` is counted the same way as the overview's `total_sessions`. `canvases` lists sections, and their\nids are valid `canvasId` inputs for `viewers` and `sessions`.\n\n`viewer_visits` does both steps for up to 25 viewers at once and returns per-visit, per-page time; use it for\n\"which pages did each viewer look at\", and `format: \"csv\"` when the user wants a file.\n\n## Per-user usage\n\"Which files did this user open\" is `files` with `userEmails`; \"who opened this file\" is `file_users`; time per page\nof a file is `file_pages`; reps' canvas usage is `canvas_users` (internal users only \u2014 external DSR viewers are\n`viewers` / `viewer_visits`).\n\n## AI Roleplay\nRoleplay results are separate tools (`pitcher_roleplay_*`): kpis, sessions and rep_summary for instance or org admins,\nteam (assignment completion; admins see every assignment, coaching managers their own), session_get and\nscenarios_list. A session with no score is unscored, never 0.\n\n## Notes\n- `popularity` maps to the canvases view-analysis endpoint (there is no separate popularity route).\n- Results can include user identities/emails \u2014 treat as customer data and handle per policy.\n";
3
3
  export declare const analyticsCatalogResource: ResourceDescriptor;
@@ -1,25 +1,26 @@
1
1
  // A model-facing catalog of the analytics verbs exposed as pitcher_analytics_* tools. It exists so a
2
2
  // host can read ONE resource to learn which verb answers which question, the time model each uses, and
3
- // the key tier each needs — instead of guessing from 10 tool descriptions. Distilled from the analytics
3
+ // the key tier each needs — instead of guessing from 14 tool descriptions. Distilled from the analytics
4
4
  // runner contracts in src/commands/analytics.ts (the authoritative source); keep in sync on route/gate
5
5
  // changes. Definitions mirror the platform analytics vocabulary.
6
6
  export const ANALYTICS_CATALOG = `# Pitcher analytics catalog
7
7
 
8
- Ten read-only verbs, each an MCP tool named \`pitcher_analytics_<verb>\`. Pick the verb that matches the
8
+ Fourteen read-only verbs, each an MCP tool named \`pitcher_analytics_<verb>\`. Pick the verb that matches the
9
9
  question, then call its tool. All are read-only; org scope + auth come from the server's API key (never a
10
10
  tool input) — you only pass \`instanceId\` (and, where noted, \`canvasId\`/\`userEmail\`).
11
11
 
12
12
  ## Key tiers
13
13
  - **org-admin key required:** org, popularity, users, files, canvases
14
14
  - **instance-admin or org-admin:** instance
15
- - **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions
15
+ - **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions, viewer_visits
16
+ - **instance editor, instance admin or org admin:** file_users, file_pages, canvas_users
16
17
 
17
18
  ## Time model
18
19
  - **Rollup (no window):** org, instance — lifetime/current totals.
19
20
  - **Date-range, 30-day default:** kpi (activity types need dates), engagement, popularity, \`users\` with
20
21
  \`stats: true\`, files, canvases — omit dates to get the last 30 days. (\`users\` without \`stats\` is a
21
- full roster with no window.)
22
- - **Date-range, server default window:** viewers, sessions — omit dates and the service picks the window.
22
+ full roster with no window.) Also file_users, file_pages, canvas_users.
23
+ - **Date-range, server default window:** viewers, sessions, viewer_visits — omit dates and the service picks the window.
23
24
 
24
25
  ## Verbs
25
26
 
@@ -35,6 +36,10 @@ tool input) — you only pass \`instanceId\` (and, where noted, \`canvasId\`/\`u
35
36
  | \`canvases\` | Per-section canvas engagement list | date-range | org-admin | "How are my canvas sections performing?" |
36
37
  | \`viewers\` | Who viewed a specific DSR canvas (per-viewer engagement) | date-range | member | "Who viewed this DSR, and how much?" |
37
38
  | \`sessions\` | One viewer's session event timeline for a DSR canvas (raw) | date-range | member | "What did this viewer do inside the DSR?" |
39
+ | \`file_users\` | Who opened ONE file: opens, seconds, days active per user | date-range | editor | "Who opened the pricing PDF, and for how long?" |
40
+ | \`file_pages\` | Time per page of ONE file (one user with a single \`userEmails\`) | date-range | editor | "Which pages of this file do reps spend time on?" |
41
+ | \`canvas_users\` | Per user and canvas: visits, seconds; with \`canvasId\`, seconds per page (section) | date-range | editor | "Which reps presented this deck, and which slides?" |
42
+ | \`viewer_visits\` | Per DSR viewer and visit: sections, files and pages with seconds; \`format: csv\` to export | date-range | member | "Export who looked at which pages of this DSR" |
38
43
 
39
44
  ## Shaping
40
45
  Row-heavy verbs (engagement, popularity, users, files, canvases, viewers) return a capped envelope:
@@ -50,6 +55,19 @@ A session is one \`Entered\` of the queried id itself (a canvas or a section), p
50
55
  \`session_count\` is counted the same way as the overview's \`total_sessions\`. \`canvases\` lists sections, and their
51
56
  ids are valid \`canvasId\` inputs for \`viewers\` and \`sessions\`.
52
57
 
58
+ \`viewer_visits\` does both steps for up to 25 viewers at once and returns per-visit, per-page time; use it for
59
+ "which pages did each viewer look at", and \`format: "csv"\` when the user wants a file.
60
+
61
+ ## Per-user usage
62
+ "Which files did this user open" is \`files\` with \`userEmails\`; "who opened this file" is \`file_users\`; time per page
63
+ of a file is \`file_pages\`; reps' canvas usage is \`canvas_users\` (internal users only — external DSR viewers are
64
+ \`viewers\` / \`viewer_visits\`).
65
+
66
+ ## AI Roleplay
67
+ Roleplay results are separate tools (\`pitcher_roleplay_*\`): kpis, sessions and rep_summary for instance or org admins,
68
+ team (assignment completion; admins see every assignment, coaching managers their own), session_get and
69
+ scenarios_list. A session with no score is unscored, never 0.
70
+
53
71
  ## Notes
54
72
  - \`popularity\` maps to the canvases view-analysis endpoint (there is no separate popularity route).
55
73
  - Results can include user identities/emails — treat as customer data and handle per policy.
@@ -4,6 +4,6 @@ import { analyticsCatalogResource } from './analyticsCatalog.js';
4
4
  // Every resource the server can expose. Mirrors mcp/registry.ts (allTools).
5
5
  export const allResources = [
6
6
  contentDesignResource,
7
- ldCanvasDesignResource,
8
- analyticsCatalogResource,
7
+ { ...ldCanvasDesignResource, group: 'ld' },
8
+ { ...analyticsCatalogResource, group: 'analytics' },
9
9
  ];
@@ -1,3 +1,4 @@
1
+ import type { ToolGroup } from '../groups.js';
1
2
  export interface ResourceDescriptor {
2
3
  name: string;
3
4
  uri: string;
@@ -5,4 +6,6 @@ export interface ResourceDescriptor {
5
6
  description: string;
6
7
  mimeType: string;
7
8
  read: () => string;
9
+ /** PIT-10114: hidden when this group is off. Unset = always served (static text no group owns). */
10
+ group?: ToolGroup;
8
11
  }
@@ -10,5 +10,6 @@ export interface CallToolResultShape {
10
10
  [key: string]: unknown;
11
11
  }
12
12
  export declare const MAX_RESULT_BYTES = 900000;
13
+ export declare const MAX_TEXT_BYTES = 8000;
13
14
  export declare function toErrorResult(code: string, message: string): CallToolResultShape;
14
- export declare function envelopeToToolResult(env: Envelope<unknown>, maxBytes?: number): CallToolResultShape;
15
+ export declare function envelopeToToolResult(env: Envelope<unknown>, maxBytes?: number, toText?: (data: unknown) => string, textMaxBytes?: number): CallToolResultShape;
@@ -3,9 +3,9 @@
3
3
  export const MAX_RESULT_BYTES = 900000;
4
4
  // Cap any single variable-length string (a server error message, a warning) so one huge value
5
5
  // can't blow the size budget regardless of which tier it lands in.
6
- const MAX_TEXT_BYTES = 8000;
7
- function clampText(s) {
8
- return Buffer.byteLength(s, 'utf8') <= MAX_TEXT_BYTES ? s : `${s.slice(0, MAX_TEXT_BYTES)}… (truncated)`;
6
+ export const MAX_TEXT_BYTES = 8000;
7
+ function clampText(s, max = MAX_TEXT_BYTES) {
8
+ return Buffer.byteLength(s, 'utf8') <= max ? s : `${s.slice(0, max)}… (truncated)`;
9
9
  }
10
10
  function isPlainObject(v) {
11
11
  return typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -21,7 +21,7 @@ function countItems(data) {
21
21
  export function toErrorResult(code, message) {
22
22
  return { content: [{ type: 'text', text: clampText(`${code}: ${message}`) }], isError: true };
23
23
  }
24
- export function envelopeToToolResult(env, maxBytes = MAX_RESULT_BYTES) {
24
+ export function envelopeToToolResult(env, maxBytes = MAX_RESULT_BYTES, toText, textMaxBytes = MAX_TEXT_BYTES) {
25
25
  if (!env.ok) {
26
26
  return toErrorResult(env.error?.code ?? 'UNKNOWN', env.error?.message ?? 'unknown error');
27
27
  }
@@ -39,8 +39,10 @@ export function envelopeToToolResult(env, maxBytes = MAX_RESULT_BYTES) {
39
39
  // counted — rather than projecting from the raw data size. maxBytes keeps headroom under the
40
40
  // host cap for the outer JSON-RPC framing around this result.
41
41
  const fits = (candidate) => Buffer.byteLength(JSON.stringify(candidate), 'utf8') <= maxBytes;
42
- // Tier 1: full payload with the default dual representation (text + structuredContent).
43
- const dual = { content: withWarnings([{ type: 'text', text: full }]), isError: false };
42
+ // Tier 1: full payload with the default dual representation (text + structuredContent). An MCP Apps tool
43
+ // (`toText`) sends the model a short text instead of the full JSON, so the payload is not sent twice.
44
+ const text = toText ? clampText(toText(env.data), textMaxBytes) : full;
45
+ const dual = { content: withWarnings([{ type: 'text', text }]), isError: false };
44
46
  if (obj)
45
47
  dual.structuredContent = obj;
46
48
  if (fits(dual))