@loopops/mcp-server 3.42.0 → 3.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,8 +17,11 @@ import { registerPursueTools } from "./tools/pursue.js";
17
17
  import { registerAccountMasterTools } from "./tools/account-master.js";
18
18
  import { registerPeopleMasterTools } from "./tools/people-master.js";
19
19
  import { registerScoringTools } from "./tools/scoring.js";
20
+ import { registerCpqTools } from "./tools/cpq.js";
20
21
  import { registerSfdcSyncTools } from "./tools/sfdc-sync.js";
21
22
  import { registerTalTools } from "./tools/tal.js";
23
+ import { parseSkillModesResponse } from "./skills/loader.js";
24
+ import { registerSkillsAsPrompts } from "./skills/handlers.js";
22
25
  // Read our own package.json at runtime so the version baked into MCP
23
26
  // initialize-handshake `serverInfo.version` matches the published npm
24
27
  // version. Was hardcoded "1.0.0" — confusing in Claude Desktop logs and
@@ -67,5 +70,22 @@ registerDeployTools(server, allowedSkills);
67
70
  registerTalTools(server, allowedSkills);
68
71
  registerScoringTools(server, allowedSkills);
69
72
  registerSfdcSyncTools(server, allowedSkills);
73
+ registerCpqTools(server, allowedSkills);
74
+ // Skills framework — fetch role-visible skills from the Loop API and
75
+ // register each as a server-provided MCP prompt. Surfaces in the
76
+ // client's `/` menu and bundles (system prompt + tool emphasis +
77
+ // on-load context). See docs/engineering/mcp-skills.md.
78
+ //
79
+ // Failure here is non-fatal: tools still work; the user just won't see
80
+ // skills in the `/` menu until they reconnect.
81
+ try {
82
+ const raw = await trpcQuery("mcp.getMySkillModes");
83
+ const skillModes = parseSkillModesResponse(raw);
84
+ registerSkillsAsPrompts(server, skillModes);
85
+ console.error(`[MCP] Skills registered: ${skillModes.map((s) => s.name).join(", ") || "(none for this role)"}`);
86
+ }
87
+ catch (err) {
88
+ console.error("[MCP] Failed to load skill modes — continuing without skill prompts:", err instanceof Error ? err.message : String(err));
89
+ }
70
90
  const transport = new StdioServerTransport();
71
91
  await server.connect(transport);
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { Skill } from "./loader.js";
3
+ /**
4
+ * Wire each skill into the MCP server as a server-provided prompt.
5
+ *
6
+ * Clients (Claude Desktop, Claude Code, Codex CLI) discover these via
7
+ * the MCP `prompts/list` request and render them in their `/` menu.
8
+ * When the user picks a skill, the client calls `prompts/get` and the
9
+ * server returns the system prompt + tool hints below.
10
+ *
11
+ * Note: this registers prompts at server startup. If skills.yaml changes
12
+ * during a session, the user must reconnect to pick up the change. A
13
+ * future `prompts/list_changed` notification could make this hot-reload,
14
+ * but the cost of that complexity isn't worth it yet — skill changes
15
+ * are infrequent.
16
+ */
17
+ export declare function registerSkillsAsPrompts(server: McpServer, skills: Skill[]): void;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Wire each skill into the MCP server as a server-provided prompt.
3
+ *
4
+ * Clients (Claude Desktop, Claude Code, Codex CLI) discover these via
5
+ * the MCP `prompts/list` request and render them in their `/` menu.
6
+ * When the user picks a skill, the client calls `prompts/get` and the
7
+ * server returns the system prompt + tool hints below.
8
+ *
9
+ * Note: this registers prompts at server startup. If skills.yaml changes
10
+ * during a session, the user must reconnect to pick up the change. A
11
+ * future `prompts/list_changed` notification could make this hot-reload,
12
+ * but the cost of that complexity isn't worth it yet — skill changes
13
+ * are infrequent.
14
+ */
15
+ export function registerSkillsAsPrompts(server, skills) {
16
+ for (const skill of skills) {
17
+ server.registerPrompt(skill.name, {
18
+ title: skill.title,
19
+ description: skill.description,
20
+ // No prompt args today — skills load context themselves via
21
+ // on_load + tools_emphasized. If we add a parametric skill in
22
+ // the future (e.g., `quote_review` taking a quoteId), expand
23
+ // here.
24
+ }, async () => {
25
+ return {
26
+ messages: [
27
+ {
28
+ role: "user",
29
+ content: {
30
+ type: "text",
31
+ text: buildPromptText(skill),
32
+ },
33
+ },
34
+ ],
35
+ };
36
+ });
37
+ }
38
+ }
39
+ /**
40
+ * Compose the final text payload returned to the MCP client. The user
41
+ * receives this as the opening turn of the skill conversation.
42
+ *
43
+ * Three sections:
44
+ * 1. The skill's `system_prompt` verbatim
45
+ * 2. Tool hints (if any) — Claude reads these and prioritizes
46
+ * 3. On-load hints (if any) — instructs Claude to call these
47
+ * proactively to load context before responding
48
+ */
49
+ function buildPromptText(skill) {
50
+ const sections = [skill.system_prompt.trim()];
51
+ if (skill.tools_emphasized.length > 0) {
52
+ sections.push(`\n---\n**Tools to favor for this skill** (others remain callable but these are the primary surface):\n` +
53
+ skill.tools_emphasized.map((t) => `- \`${t}\``).join("\n"));
54
+ }
55
+ if (skill.on_load.length > 0) {
56
+ sections.push(`\n---\n**On entering this skill, call these tools first** to load context:\n` +
57
+ skill.on_load
58
+ .map((c) => {
59
+ const argsStr = Object.keys(c.args).length === 0
60
+ ? "(no args)"
61
+ : `with args ${JSON.stringify(c.args)}`;
62
+ return `1. \`${c.tool}\` ${argsStr}`;
63
+ })
64
+ .join("\n"));
65
+ }
66
+ return sections.join("\n");
67
+ }
@@ -0,0 +1,194 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Skill schema for the Loop MCP skills framework.
4
+ *
5
+ * A skill is a named workflow mode that bundles a system prompt with a
6
+ * subset of available tools. Skills surface in the user's MCP client as
7
+ * server-provided prompts (Claude Desktop's `/` menu, Claude Code slash
8
+ * commands, Codex CLI prompts).
9
+ *
10
+ * Source of truth: `config/mcp/skills.yaml` in the loop repo. The Loop
11
+ * API exposes role-filtered skills via `mcp.getMySkillModes`; the MCP
12
+ * server then registers each as a prompt at connect time.
13
+ *
14
+ * See docs/engineering/mcp-skills.md for the framework + tool-vs-skill
15
+ * decision guide.
16
+ */
17
+ export declare const skillSchema: z.ZodObject<{
18
+ /** Slug used to invoke the skill (e.g., `/cpq_coach`). Lowercase + underscores only. */
19
+ name: z.ZodString;
20
+ /** Display title shown in the client's prompt menu. */
21
+ title: z.ZodString;
22
+ /** One-line hover description in the client's prompt menu. */
23
+ description: z.ZodString;
24
+ /**
25
+ * The "you are…" prompt that defines the skill's voice + workflow.
26
+ * Multiline; treat as a markdown-friendly system prompt.
27
+ */
28
+ system_prompt: z.ZodString;
29
+ /**
30
+ * Tools the agent should focus on for this skill. Not exclusive —
31
+ * other tools remain callable. A HINT to Claude; user doesn't see it.
32
+ */
33
+ tools_emphasized: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
34
+ /**
35
+ * Optional turn-1 tool invocations. Rendered as "call these first"
36
+ * hints in the prompt response so Claude loads context proactively.
37
+ *
38
+ * Future: a stricter "auto-call" mode where the server pre-invokes
39
+ * these and returns the result inline.
40
+ */
41
+ on_load: z.ZodDefault<z.ZodArray<z.ZodObject<{
42
+ tool: z.ZodString;
43
+ args: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ tool: string;
46
+ args: Record<string, unknown>;
47
+ }, {
48
+ tool: string;
49
+ args?: Record<string, unknown> | undefined;
50
+ }>, "many">>;
51
+ /**
52
+ * Roles that can see/invoke this skill. If omitted, all authenticated
53
+ * roles can see it. Same role names as the MCP role system: field /
54
+ * ops / dev / admin.
55
+ */
56
+ roles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
57
+ }, "strip", z.ZodTypeAny, {
58
+ description: string;
59
+ name: string;
60
+ title: string;
61
+ system_prompt: string;
62
+ tools_emphasized: string[];
63
+ on_load: {
64
+ tool: string;
65
+ args: Record<string, unknown>;
66
+ }[];
67
+ roles?: string[] | undefined;
68
+ }, {
69
+ description: string;
70
+ name: string;
71
+ title: string;
72
+ system_prompt: string;
73
+ tools_emphasized?: string[] | undefined;
74
+ on_load?: {
75
+ tool: string;
76
+ args?: Record<string, unknown> | undefined;
77
+ }[] | undefined;
78
+ roles?: string[] | undefined;
79
+ }>;
80
+ export type Skill = z.infer<typeof skillSchema>;
81
+ declare const skillsFileSchema: z.ZodObject<{
82
+ version: z.ZodString;
83
+ skills: z.ZodArray<z.ZodObject<{
84
+ /** Slug used to invoke the skill (e.g., `/cpq_coach`). Lowercase + underscores only. */
85
+ name: z.ZodString;
86
+ /** Display title shown in the client's prompt menu. */
87
+ title: z.ZodString;
88
+ /** One-line hover description in the client's prompt menu. */
89
+ description: z.ZodString;
90
+ /**
91
+ * The "you are…" prompt that defines the skill's voice + workflow.
92
+ * Multiline; treat as a markdown-friendly system prompt.
93
+ */
94
+ system_prompt: z.ZodString;
95
+ /**
96
+ * Tools the agent should focus on for this skill. Not exclusive —
97
+ * other tools remain callable. A HINT to Claude; user doesn't see it.
98
+ */
99
+ tools_emphasized: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
100
+ /**
101
+ * Optional turn-1 tool invocations. Rendered as "call these first"
102
+ * hints in the prompt response so Claude loads context proactively.
103
+ *
104
+ * Future: a stricter "auto-call" mode where the server pre-invokes
105
+ * these and returns the result inline.
106
+ */
107
+ on_load: z.ZodDefault<z.ZodArray<z.ZodObject<{
108
+ tool: z.ZodString;
109
+ args: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
110
+ }, "strip", z.ZodTypeAny, {
111
+ tool: string;
112
+ args: Record<string, unknown>;
113
+ }, {
114
+ tool: string;
115
+ args?: Record<string, unknown> | undefined;
116
+ }>, "many">>;
117
+ /**
118
+ * Roles that can see/invoke this skill. If omitted, all authenticated
119
+ * roles can see it. Same role names as the MCP role system: field /
120
+ * ops / dev / admin.
121
+ */
122
+ roles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
123
+ }, "strip", z.ZodTypeAny, {
124
+ description: string;
125
+ name: string;
126
+ title: string;
127
+ system_prompt: string;
128
+ tools_emphasized: string[];
129
+ on_load: {
130
+ tool: string;
131
+ args: Record<string, unknown>;
132
+ }[];
133
+ roles?: string[] | undefined;
134
+ }, {
135
+ description: string;
136
+ name: string;
137
+ title: string;
138
+ system_prompt: string;
139
+ tools_emphasized?: string[] | undefined;
140
+ on_load?: {
141
+ tool: string;
142
+ args?: Record<string, unknown> | undefined;
143
+ }[] | undefined;
144
+ roles?: string[] | undefined;
145
+ }>, "many">;
146
+ }, "strip", z.ZodTypeAny, {
147
+ version: string;
148
+ skills: {
149
+ description: string;
150
+ name: string;
151
+ title: string;
152
+ system_prompt: string;
153
+ tools_emphasized: string[];
154
+ on_load: {
155
+ tool: string;
156
+ args: Record<string, unknown>;
157
+ }[];
158
+ roles?: string[] | undefined;
159
+ }[];
160
+ }, {
161
+ version: string;
162
+ skills: {
163
+ description: string;
164
+ name: string;
165
+ title: string;
166
+ system_prompt: string;
167
+ tools_emphasized?: string[] | undefined;
168
+ on_load?: {
169
+ tool: string;
170
+ args?: Record<string, unknown> | undefined;
171
+ }[] | undefined;
172
+ roles?: string[] | undefined;
173
+ }[];
174
+ }>;
175
+ export type SkillsFile = z.infer<typeof skillsFileSchema>;
176
+ /**
177
+ * Load + validate `config/mcp/skills.yaml`. Used server-side (Loop API)
178
+ * to source the skills registry; throws on schema violation so bad
179
+ * config fails the build/start rather than serving a broken skill.
180
+ */
181
+ export declare function loadSkillsYaml(yamlPath: string): SkillsFile;
182
+ /**
183
+ * Filter skills by the caller's role. Skills with no `roles` constraint
184
+ * are visible to everyone authenticated.
185
+ */
186
+ export declare function skillsForRole(skills: Skill[], role: string): Skill[];
187
+ /**
188
+ * Validate an API response from `mcp.getMySkillModes`. Used client-side
189
+ * (the @loopops/mcp-server package) when fetching the role-filtered
190
+ * skills from the Loop API. Returns an empty array on shape mismatch
191
+ * so a broken API response doesn't crash the MCP server.
192
+ */
193
+ export declare function parseSkillModesResponse(raw: unknown): Skill[];
194
+ export {};
@@ -0,0 +1,93 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import yaml from "yaml";
4
+ /**
5
+ * Skill schema for the Loop MCP skills framework.
6
+ *
7
+ * A skill is a named workflow mode that bundles a system prompt with a
8
+ * subset of available tools. Skills surface in the user's MCP client as
9
+ * server-provided prompts (Claude Desktop's `/` menu, Claude Code slash
10
+ * commands, Codex CLI prompts).
11
+ *
12
+ * Source of truth: `config/mcp/skills.yaml` in the loop repo. The Loop
13
+ * API exposes role-filtered skills via `mcp.getMySkillModes`; the MCP
14
+ * server then registers each as a prompt at connect time.
15
+ *
16
+ * See docs/engineering/mcp-skills.md for the framework + tool-vs-skill
17
+ * decision guide.
18
+ */
19
+ export const skillSchema = z.object({
20
+ /** Slug used to invoke the skill (e.g., `/cpq_coach`). Lowercase + underscores only. */
21
+ name: z
22
+ .string()
23
+ .regex(/^[a-z][a-z0-9_]*$/, "name must be snake_case and start with a letter"),
24
+ /** Display title shown in the client's prompt menu. */
25
+ title: z.string().min(1),
26
+ /** One-line hover description in the client's prompt menu. */
27
+ description: z.string().min(1),
28
+ /**
29
+ * The "you are…" prompt that defines the skill's voice + workflow.
30
+ * Multiline; treat as a markdown-friendly system prompt.
31
+ */
32
+ system_prompt: z.string().min(1),
33
+ /**
34
+ * Tools the agent should focus on for this skill. Not exclusive —
35
+ * other tools remain callable. A HINT to Claude; user doesn't see it.
36
+ */
37
+ tools_emphasized: z.array(z.string()).default([]),
38
+ /**
39
+ * Optional turn-1 tool invocations. Rendered as "call these first"
40
+ * hints in the prompt response so Claude loads context proactively.
41
+ *
42
+ * Future: a stricter "auto-call" mode where the server pre-invokes
43
+ * these and returns the result inline.
44
+ */
45
+ on_load: z
46
+ .array(z.object({
47
+ tool: z.string(),
48
+ args: z.record(z.unknown()).default({}),
49
+ }))
50
+ .default([]),
51
+ /**
52
+ * Roles that can see/invoke this skill. If omitted, all authenticated
53
+ * roles can see it. Same role names as the MCP role system: field /
54
+ * ops / dev / admin.
55
+ */
56
+ roles: z.array(z.string()).optional(),
57
+ });
58
+ const skillsFileSchema = z.object({
59
+ version: z.string(),
60
+ skills: z.array(skillSchema),
61
+ });
62
+ /**
63
+ * Load + validate `config/mcp/skills.yaml`. Used server-side (Loop API)
64
+ * to source the skills registry; throws on schema violation so bad
65
+ * config fails the build/start rather than serving a broken skill.
66
+ */
67
+ export function loadSkillsYaml(yamlPath) {
68
+ const raw = readFileSync(yamlPath, "utf-8");
69
+ const parsed = yaml.parse(raw);
70
+ return skillsFileSchema.parse(parsed);
71
+ }
72
+ /**
73
+ * Filter skills by the caller's role. Skills with no `roles` constraint
74
+ * are visible to everyone authenticated.
75
+ */
76
+ export function skillsForRole(skills, role) {
77
+ return skills.filter((s) => !s.roles || s.roles.includes(role));
78
+ }
79
+ /**
80
+ * Validate an API response from `mcp.getMySkillModes`. Used client-side
81
+ * (the @loopops/mcp-server package) when fetching the role-filtered
82
+ * skills from the Loop API. Returns an empty array on shape mismatch
83
+ * so a broken API response doesn't crash the MCP server.
84
+ */
85
+ export function parseSkillModesResponse(raw) {
86
+ const arraySchema = z.object({ skills: z.array(skillSchema) });
87
+ const parsed = arraySchema.safeParse(raw);
88
+ if (!parsed.success) {
89
+ console.error("[skills] Unexpected response shape from mcp.getMySkillModes:", parsed.error.message);
90
+ return [];
91
+ }
92
+ return parsed.data.skills;
93
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,159 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { loadSkillsYaml, skillsForRole, parseSkillModesResponse } from "./loader.js";
7
+ function writeTempYaml(content) {
8
+ const dir = mkdtempSync(join(tmpdir(), "skills-test-"));
9
+ const path = join(dir, "skills.yaml");
10
+ writeFileSync(path, content, "utf-8");
11
+ return {
12
+ path,
13
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
14
+ };
15
+ }
16
+ test("loadSkillsYaml: parses a minimal skill", () => {
17
+ const { path, cleanup } = writeTempYaml(`
18
+ version: "0.1.0"
19
+ skills:
20
+ - name: hello
21
+ title: "Hello"
22
+ description: "A greeting skill"
23
+ system_prompt: "Say hi to the user."
24
+ `);
25
+ try {
26
+ const parsed = loadSkillsYaml(path);
27
+ assert.equal(parsed.skills.length, 1);
28
+ assert.equal(parsed.skills[0].name, "hello");
29
+ assert.deepEqual(parsed.skills[0].tools_emphasized, []);
30
+ assert.deepEqual(parsed.skills[0].on_load, []);
31
+ }
32
+ finally {
33
+ cleanup();
34
+ }
35
+ });
36
+ test("loadSkillsYaml: parses a fully-specified skill", () => {
37
+ const { path, cleanup } = writeTempYaml(`
38
+ version: "0.1.0"
39
+ skills:
40
+ - name: cpq_coach
41
+ title: "CPQ Coach"
42
+ description: "Build quotes"
43
+ system_prompt: "You are a CPQ coach."
44
+ tools_emphasized: [cpq_context, quote_show]
45
+ on_load:
46
+ - tool: cpq_context
47
+ args: {}
48
+ roles: [field, ops]
49
+ `);
50
+ try {
51
+ const parsed = loadSkillsYaml(path);
52
+ const skill = parsed.skills[0];
53
+ assert.equal(skill.name, "cpq_coach");
54
+ assert.deepEqual(skill.tools_emphasized, ["cpq_context", "quote_show"]);
55
+ assert.equal(skill.on_load[0].tool, "cpq_context");
56
+ assert.deepEqual(skill.roles, ["field", "ops"]);
57
+ }
58
+ finally {
59
+ cleanup();
60
+ }
61
+ });
62
+ test("loadSkillsYaml: rejects bad slug (caps + dashes)", () => {
63
+ const { path, cleanup } = writeTempYaml(`
64
+ version: "0.1.0"
65
+ skills:
66
+ - name: CPQ-Coach
67
+ title: "Bad"
68
+ description: "Bad"
69
+ system_prompt: "x"
70
+ `);
71
+ try {
72
+ assert.throws(() => loadSkillsYaml(path));
73
+ }
74
+ finally {
75
+ cleanup();
76
+ }
77
+ });
78
+ test("loadSkillsYaml: rejects empty system_prompt", () => {
79
+ const { path, cleanup } = writeTempYaml(`
80
+ version: "0.1.0"
81
+ skills:
82
+ - name: empty
83
+ title: "x"
84
+ description: "x"
85
+ system_prompt: ""
86
+ `);
87
+ try {
88
+ assert.throws(() => loadSkillsYaml(path));
89
+ }
90
+ finally {
91
+ cleanup();
92
+ }
93
+ });
94
+ const SKILLS = [
95
+ {
96
+ name: "public",
97
+ title: "Public",
98
+ description: "Everyone",
99
+ system_prompt: "x",
100
+ tools_emphasized: [],
101
+ on_load: [],
102
+ },
103
+ {
104
+ name: "field_only",
105
+ title: "Field Only",
106
+ description: "Field reps",
107
+ system_prompt: "x",
108
+ tools_emphasized: [],
109
+ on_load: [],
110
+ roles: ["field"],
111
+ },
112
+ {
113
+ name: "ops_admin",
114
+ title: "Ops + Admin",
115
+ description: "Privileged",
116
+ system_prompt: "x",
117
+ tools_emphasized: [],
118
+ on_load: [],
119
+ roles: ["ops", "admin"],
120
+ },
121
+ ];
122
+ test("skillsForRole: includes public + role-matching skills", () => {
123
+ const fieldSkills = skillsForRole(SKILLS, "field");
124
+ const names = fieldSkills.map((s) => s.name).sort();
125
+ assert.deepEqual(names, ["field_only", "public"]);
126
+ });
127
+ test("skillsForRole: excludes role-gated skills the caller doesn't have", () => {
128
+ const fieldSkills = skillsForRole(SKILLS, "field");
129
+ assert.equal(fieldSkills.find((s) => s.name === "ops_admin"), undefined);
130
+ });
131
+ test("skillsForRole: multi-role skills visible when caller matches any role", () => {
132
+ const adminSkills = skillsForRole(SKILLS, "admin");
133
+ assert.ok(adminSkills.some((s) => s.name === "ops_admin"));
134
+ });
135
+ test("skillsForRole: caller with no match sees only public skills", () => {
136
+ const guestSkills = skillsForRole(SKILLS, "stranger");
137
+ assert.deepEqual(guestSkills.map((s) => s.name), ["public"]);
138
+ });
139
+ test("parseSkillModesResponse: extracts skills from a well-formed response", () => {
140
+ const raw = {
141
+ skills: [
142
+ {
143
+ name: "test_skill",
144
+ title: "Test",
145
+ description: "Test skill",
146
+ system_prompt: "test",
147
+ tools_emphasized: ["a", "b"],
148
+ on_load: [],
149
+ },
150
+ ],
151
+ };
152
+ const skills = parseSkillModesResponse(raw);
153
+ assert.equal(skills.length, 1);
154
+ assert.equal(skills[0].name, "test_skill");
155
+ });
156
+ test("parseSkillModesResponse: returns empty array on bad shape", () => {
157
+ const skills = parseSkillModesResponse({ wrong: "shape" });
158
+ assert.deepEqual(skills, []);
159
+ });
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * CPQ tools — surface for the cpq_coach skill.
4
+ *
5
+ * The skill in config/mcp/skills.yaml bundles these into a workflow
6
+ * mode (system prompt + tool emphasis + auto-load context). When a
7
+ * user invokes /cpq_coach, the MCP client loads the prompt + Claude
8
+ * is told to call `cpq_context` as turn-1.
9
+ *
10
+ * Authorization gates: each tool is conditionally registered based on
11
+ * the user's role grant from config/mcp/role-tool-grants.yaml.
12
+ */
13
+ export declare function registerCpqTools(server: McpServer, allowed: Set<string>): void;
@@ -0,0 +1,18 @@
1
+ import { trpcQuery } from "../api-client.js";
2
+ import { safeTool } from "./_helpers.js";
3
+ /**
4
+ * CPQ tools — surface for the cpq_coach skill.
5
+ *
6
+ * The skill in config/mcp/skills.yaml bundles these into a workflow
7
+ * mode (system prompt + tool emphasis + auto-load context). When a
8
+ * user invokes /cpq_coach, the MCP client loads the prompt + Claude
9
+ * is told to call `cpq_context` as turn-1.
10
+ *
11
+ * Authorization gates: each tool is conditionally registered based on
12
+ * the user's role grant from config/mcp/role-tool-grants.yaml.
13
+ */
14
+ export function registerCpqTools(server, allowed) {
15
+ if (allowed.has("cpq_context")) {
16
+ server.tool("cpq_context", "Loads the full ClickHouse CPQ context: the playbook (tier guidance, sizing heuristics, commit strategy, discount approach, validation rules, common mistakes, FAQ), the product catalog (16 products with rates), and the discount schedule (commit × term grid). Call this ONCE per CPQ conversation before answering any pricing or quote-building question. The cpq_coach skill auto-loads this.", {}, safeTool(async () => trpcQuery("mcp.cpqContext")));
17
+ }
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "3.42.0",
3
+ "version": "3.43.1",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -20,18 +20,20 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsx src/index.ts",
26
+ "start": "node dist/index.js",
27
+ "prepublishOnly": "pnpm build"
28
+ },
23
29
  "dependencies": {
24
30
  "@modelcontextprotocol/sdk": "^1.12.1",
31
+ "yaml": "^2.6.0",
25
32
  "zod": "^3.24.4"
26
33
  },
27
34
  "devDependencies": {
28
35
  "@types/node": "^22.15.21",
29
36
  "tsx": "^4.19.4",
30
37
  "typescript": "^5.8.3"
31
- },
32
- "scripts": {
33
- "build": "tsc",
34
- "dev": "tsx src/index.ts",
35
- "start": "node dist/index.js"
36
38
  }
37
- }
39
+ }