spm-mcp 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@ Not just PRDs. Every document in the PM lifecycle:
21
21
  - **Execution:** User Stories, Feature Spec, Sprint Planning, PRD to Jira, Release Notes, Test Cases
22
22
  - **Discovery:** Problem Statement, Persona, Jobs-to-be-Done, Opportunity Assessment
23
23
 
24
- 30 built-in expert reviews. Or create your own with `spm_create_custom_nano_app`.
24
+ 30 built-in expert reviews. Or bring your own template with `spm_create_custom_nano_app` -- paste your favorite PRD or describe your sections, and SPM creates a personalized review in seconds.
25
25
 
26
26
  ## Quick start
27
27
 
@@ -40,7 +40,7 @@ First run auto-detects no API key and walks you through setup:
40
40
  ### Option B: Manual setup
41
41
 
42
42
  ```bash
43
- # 1. Get your key at superproductmanager.ai > Profile > Generate API Key
43
+ # 1. Get your key at superproductmanager.ai?utm_source=npm&utm_medium=readme&utm_campaign=mcp > Profile > Generate API Key
44
44
  # 2. Set it
45
45
  export SPM_API_KEY=spm_k_your_key_here
46
46
  # 3. Run
@@ -51,7 +51,7 @@ npx spm-mcp
51
51
 
52
52
  For claude.ai web users, SPM is available as a remote MCP server:
53
53
 
54
- 1. Get your API key at [superproductmanager.ai](https://superproductmanager.ai) > Profile
54
+ 1. Get your API key at [superproductmanager.ai](https://superproductmanager.ai?utm_source=npm&utm_medium=readme&utm_campaign=mcp) > Profile
55
55
  2. In claude.ai, go to Settings > Integrations > Add MCP Server
56
56
  3. URL: `https://spm-mcp.superproductmanager.ai/mcp`
57
57
  4. Add header: `X-SPM-API-Key: spm_k_your_key_here`
@@ -60,12 +60,12 @@ For claude.ai web users, SPM is available as a remote MCP server:
60
60
 
61
61
  | Tool | What it does |
62
62
  |------|-------------|
63
- | `spm_list_nano_apps` | Discover available expert reviews (30 built-in + your custom ones) |
64
- | `spm_analyze` | Score a document against domain-specific expert expectations. Every gap scored 0-1.0 with evidence. |
65
- | `spm_clarify` | Get decision-forcing questions for the weakest gaps. Questions escalate when you give vague answers. |
63
+ | `spm_create_custom_nano_app` | **Start here.** Paste your favorite PRD, describe your template sections, or pass evaluation criteria. Creates a personalized review that uses your terminology and covers your sections. |
64
+ | `spm_list_nano_apps` | Browse 30 built-in expert reviews if you don't have your own template. |
65
+ | `spm_analyze` | Score a document against expert expectations. Every gap scored 0-1.0 with evidence. |
66
+ | `spm_clarify` | Decision-forcing questions for the weakest gaps. Questions escalate when you give vague answers. |
66
67
  | `spm_evaluate` | Re-score gaps after clarification rounds. Tracks progress. Use after every 3 rounds of `spm_clarify`. |
67
68
  | `spm_improve` | Generate paste-ready improvements grounded in your answers, not AI hallucination. |
68
- | `spm_create_custom_nano_app` | Create a custom expert review for any document type not covered by the built-in 30. |
69
69
 
70
70
  ## How it works
71
71
 
@@ -79,7 +79,24 @@ Your document
79
79
 
80
80
  The clarification questions are the product. They surface the assumptions you've been carrying without examining. When you dodge, they escalate: evidence first, then action directives, then assumptions made on your behalf. Like a principal PM review, not a chatbot.
81
81
 
82
- ## Example workflow
82
+ ## Example: Bring your own template
83
+
84
+ ```
85
+ You: Here's my team's PRD template [pastes PRD]
86
+ SPM: Detected 9 sections. Created "PRD Review" with 5 expectations:
87
+
88
+ Expectation | Rules | Checks
89
+ Problem & Goal | Problem, Goal | Data-backed problem, measurable goal
90
+ Users & Solution | Target Users, Proposed Solution| Specific personas, buildable solution
91
+ Metrics & Timeline | Success Metrics, Timeline | Baselines, phased delivery
92
+ Technical & Risks | Technical Approach, Risks | Dependencies, mitigations
93
+ Out of Scope | Out of Scope | Explicit boundary list
94
+
95
+ You: Looks good. Analyze this PRD.
96
+ SPM: [scores against YOUR expectations, not generic ones]
97
+ ```
98
+
99
+ ## Example: Built-in review
83
100
 
84
101
  ```
85
102
  You: Analyze this PRD with prd_critique
@@ -160,13 +177,13 @@ API key resolution order: `SPM_API_KEY` env var > `~/.spm/config.json` > setup p
160
177
 
161
178
  ## Also available as
162
179
 
163
- - **Chrome extension.** Reviews documents inside Google Docs, Notion, ClickUp, Linear. [Install from Chrome Web Store](https://chromewebstore.google.com/detail/super-product-manager/ocpjfedoogmpbkhpojdkfdimkbiamihg)
164
- - **Web app.** Paste any document, full analysis in 30 seconds. [superproductmanager.ai](https://superproductmanager.ai)
180
+ - **Chrome extension.** Reviews documents inside Google Docs, Notion, ClickUp, Linear. [Install from Chrome Web Store](https://chromewebstore.google.com/detail/super-product-manager/ocpjfedoogmpbkhpojdkfdimkbiamihg?utm_source=npm&utm_medium=readme&utm_campaign=mcp)
181
+ - **Web app.** Paste any document, full analysis in 30 seconds. [superproductmanager.ai](https://superproductmanager.ai?utm_source=npm&utm_medium=readme&utm_campaign=mcp)
165
182
 
166
183
  ## Links
167
184
 
168
- - [Website](https://superproductmanager.ai)
169
- - [Chrome Extension](https://chromewebstore.google.com/detail/super-product-manager/ocpjfedoogmpbkhpojdkfdimkbiamihg)
185
+ - [Website](https://superproductmanager.ai?utm_source=npm&utm_medium=readme&utm_campaign=mcp)
186
+ - [Chrome Extension](https://chromewebstore.google.com/detail/super-product-manager/ocpjfedoogmpbkhpojdkfdimkbiamihg?utm_source=npm&utm_medium=readme&utm_campaign=mcp)
170
187
 
171
188
  ## License
172
189
 
package/dist/src/index.js CHANGED
@@ -19,6 +19,7 @@ import { handleClarify } from './tools/clarify.js';
19
19
  import { handleEvaluate } from './tools/evaluate.js';
20
20
  import { handleImprove } from './tools/improve.js';
21
21
  import { handleCreateCustomNanoApp } from './tools/create-custom-nano-app.js';
22
+ import { handleWorkspace } from './tools/workspace.js';
22
23
  import { SpmApiError } from './client/spm-api.js';
23
24
  function logToolCall(tool, input) {
24
25
  const summary = {};
@@ -42,9 +43,15 @@ export function createSpmMcpServer(options) {
42
43
  });
43
44
  const apiKey = options?.apiKey;
44
45
  // Tool: spm_list_nano_apps
45
- server.tool('spm_list_nano_apps', 'List all available SPM nano app templates for product document analysis. ' +
46
+ server.tool('spm_list_nano_apps', 'List available SPM nano app templates. ' +
47
+ 'IMPORTANT: Do NOT call this as your first action. Instead:\n' +
48
+ '1. Ask the user what document or task they are working on.\n' +
49
+ '2. Check if they have an existing template, PRD, or evaluation criteria ' +
50
+ '(look in workspace files, Claude project instructions, or ask).\n' +
51
+ '3. If they have their own template or a favorite PRD, use spm_create_custom_nano_app instead.\n' +
52
+ '4. Only call this tool if the user explicitly wants to browse available templates.\n' +
46
53
  'Returns template keys, names, descriptions, and categories. ' +
47
- 'Use this to discover which analysis types are available before calling spm_analyze.', {}, async () => {
54
+ 'DISPLAY: Show the top 5-6 most popular first. Only show the full list if the user asks for it.', {}, async () => {
48
55
  try {
49
56
  logToolCall('spm_list_nano_apps', {});
50
57
  const result = await handleListNanoApps(apiKey);
@@ -166,18 +173,31 @@ export function createSpmMcpServer(options) {
166
173
  }
167
174
  });
168
175
  // Tool: spm_create_custom_nano_app
169
- server.tool('spm_create_custom_nano_app', 'Create a custom nano app when the user\'s document doesn\'t match any existing nano app ' +
170
- '(call spm_list_nano_apps first to check).\n\n' +
171
- 'BEFORE calling this tool, gather preferences from the user:\n' +
172
- '1. WHAT TO EVALUATE → expectations + rules\n' +
173
- '2. DOMAIN LENS domainContext\n' +
174
- '3. GOOD vs BAD SIGNALS reviewer examples\n' +
175
- '4. CLARIFICATION STYLE clarification examples + instructions\n' +
176
- '5. IMPROVEMENT FORMAT improviser instructions\n\n' +
177
- 'IMPORTANT: Do NOT run a rigid 5-question interview. Extract what you can from the user\'s ' +
178
- 'initial message. Only ask 1-2 clarifying questions for what\'s genuinely missing. ' +
179
- 'Bias toward ACTION over interrogation. The endpoint handles missing preferences gracefully.', {
180
- description: z.string().describe('What kind of document this nano app analyzes and what it should evaluate'),
176
+ server.tool('spm_create_custom_nano_app', 'Create a personalized nano app from the user\'s own template, PRD, or evaluation criteria. ' +
177
+ 'This is the PREFERRED path for new users who have their own standards.\n\n' +
178
+ 'The endpoint accepts MULTIPLE input types prepare the best input:\n\n' +
179
+ 'BEST (structured description + preferences):\n' +
180
+ ' description: "Evaluate PRDs with 9 sections: Problem, Goal, Target Users, Solution, ' +
181
+ 'Success Metrics, Timeline, Technical Approach, Risks, Out of Scope"\n' +
182
+ ' preferences: { domain_lens: "...", good_bad_signals: "..." }\n\n' +
183
+ 'GOOD (raw PRD or document pasted as description):\n' +
184
+ ' description: <paste the user\'s actual PRD or template document here>\n' +
185
+ ' The endpoint will extract sections and create generic rules from the structure.\n\n' +
186
+ 'OK (bare description):\n' +
187
+ ' description: "Evaluate product strategy for investment readiness"\n\n' +
188
+ 'INPUT PREPARATION — How to build the best input:\n' +
189
+ '1. If the user has a template/PRD, read it and list its sections explicitly in the description ' +
190
+ '(e.g., "9 sections: Problem, Goal, Target Users..."). This produces the best results.\n' +
191
+ '2. Add preferences.domain_lens with who is reviewing and what matters.\n' +
192
+ '3. Add preferences.good_bad_signals with concrete examples of quality.\n' +
193
+ '4. If the user provides explicit evaluation criteria or rules, pass them in the description — ' +
194
+ 'the endpoint will use them as-is, not reinterpret.\n\n' +
195
+ 'DISPLAY: After creation, show a table: Expectation | Rules | What it checks. ' +
196
+ 'Ask the user to confirm before proceeding to spm_analyze. Do NOT show the scoring rubric — ' +
197
+ 'users see it in action when they analyze a document.', {
198
+ description: z.string().describe('BEST: List sections explicitly ("Evaluate PRDs with 9 sections: Problem, Goal, ..."). ' +
199
+ 'ALSO WORKS: Paste a raw PRD/template document — sections will be auto-extracted. ' +
200
+ 'ALSO WORKS: Short description ("Evaluate product strategy for investment readiness").'),
181
201
  name: z.string().optional().describe('Human-readable name for the nano app (auto-generated if omitted)'),
182
202
  preferences: z.object({
183
203
  domain_lens: z.string().optional().describe('Domain context and expertise lens'),
@@ -197,6 +217,27 @@ export function createSpmMcpServer(options) {
197
217
  return errorResponse(err);
198
218
  }
199
219
  });
220
+ // Tool: spm_read_workspace
221
+ server.tool('spm_read_workspace', 'Read or write files from the user\'s local workspace to gather project context or record decisions. ' +
222
+ 'Three modes: (1) "scan" — auto-discover known config files (CLAUDE.md, SKILL.md, README.md)\n' +
223
+ '(2) "read" — read a specific .md file by relative path\n' +
224
+ '(3) "write" — write content to a .md file by relative path (e.g., to record a system-of-record decision summary)\n' +
225
+ 'All paths are restricted to the project directory for security, and ONLY .md files are allowed.', {
226
+ mode: z.enum(['scan', 'read', 'write']).describe('"scan" to discover files, "read" to get contents, "write" to save contents'),
227
+ path: z.string().optional().describe('Relative path to the file (required when mode is "read" or "write"). Must be a .md file.'),
228
+ content: z.string().optional().describe('The markdown content to write to the file (required when mode is "write").'),
229
+ }, async (input) => {
230
+ try {
231
+ logToolCall('spm_read_workspace', input);
232
+ const result = await handleWorkspace(input);
233
+ return {
234
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
235
+ };
236
+ }
237
+ catch (err) {
238
+ return errorResponse(err);
239
+ }
240
+ });
200
241
  return server;
201
242
  }
202
243
  function errorResponse(err) {
package/dist/src/stdio.js CHANGED
@@ -18,7 +18,7 @@ import { homedir } from 'os';
18
18
  import { createInterface } from 'readline';
19
19
  const CONFIG_DIR = join(homedir(), '.spm');
20
20
  const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
21
- const SETUP_URL = 'https://superproductmanager.web.app/#/profile';
21
+ const SETUP_URL = 'https://superproductmanager.web.app/#/profile?utm_source=npm&utm_medium=cli_setup&utm_campaign=mcp';
22
22
  function isInteractive() {
23
23
  return process.stdin.isTTY === true;
24
24
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * spm_read_workspace — Local workspace file reader.
3
+ *
4
+ * Reads files from the user's workspace (cwd) to provide project context.
5
+ * Two modes:
6
+ * - scan: auto-discover known config/doc files and return a manifest
7
+ * - read: read a specific file by relative path
8
+ *
9
+ * Security:
10
+ * - All paths resolved and validated to stay within cwd
11
+ * - Only .md files are allowed (no .env, .json, source code, etc.)
12
+ */
13
+ export interface WorkspaceInput {
14
+ mode: 'scan' | 'read' | 'write';
15
+ path?: string;
16
+ content?: string;
17
+ }
18
+ export declare function handleWorkspace(input: WorkspaceInput): Promise<unknown>;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * spm_read_workspace — Local workspace file reader.
3
+ *
4
+ * Reads files from the user's workspace (cwd) to provide project context.
5
+ * Two modes:
6
+ * - scan: auto-discover known config/doc files and return a manifest
7
+ * - read: read a specific file by relative path
8
+ *
9
+ * Security:
10
+ * - All paths resolved and validated to stay within cwd
11
+ * - Only .md files are allowed (no .env, .json, source code, etc.)
12
+ */
13
+ import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
14
+ import { resolve, relative, join, extname, dirname } from 'path';
15
+ const log = (msg) => process.stderr.write(`[spm-mcp:workspace] ${msg}\n`);
16
+ /** Only markdown files are allowed */
17
+ const ALLOWED_EXTENSIONS = ['.md'];
18
+ /** Files to auto-discover in scan mode */
19
+ const KNOWN_FILES = [
20
+ 'CLAUDE.md',
21
+ 'claude.md',
22
+ 'README.md',
23
+ '.github/copilot-instructions.md',
24
+ ];
25
+ /** Directories to scan for skill files */
26
+ const SKILL_DIRS = [
27
+ '.claude/skills',
28
+ '.gemini/skills',
29
+ ];
30
+ /** Max file size to read (100 KB) — skip huge files */
31
+ const MAX_FILE_SIZE = 100 * 1024;
32
+ // ─── Security ───────────────────────────────────────────────────────
33
+ function safePath(relativePath) {
34
+ const cwd = process.cwd();
35
+ const resolved = resolve(cwd, relativePath);
36
+ const rel = relative(cwd, resolved);
37
+ if (rel.startsWith('..') || rel === resolved) {
38
+ log(`BLOCKED path traversal: ${relativePath}`);
39
+ return null;
40
+ }
41
+ return resolved;
42
+ }
43
+ function scanWorkspace() {
44
+ const cwd = process.cwd();
45
+ const found = [];
46
+ // Check known files
47
+ for (const file of KNOWN_FILES) {
48
+ const full = join(cwd, file);
49
+ if (existsSync(full)) {
50
+ const stat = statSync(full);
51
+ if (stat.isFile()) {
52
+ found.push({ path: file, size: stat.size, type: 'config' });
53
+ }
54
+ }
55
+ }
56
+ // Scan skill directories for SKILL.md files
57
+ for (const dir of SKILL_DIRS) {
58
+ const fullDir = join(cwd, dir);
59
+ if (existsSync(fullDir) && statSync(fullDir).isDirectory()) {
60
+ try {
61
+ const entries = readdirSync(fullDir);
62
+ for (const entry of entries) {
63
+ const skillPath = join(fullDir, entry, 'SKILL.md');
64
+ if (existsSync(skillPath)) {
65
+ const stat = statSync(skillPath);
66
+ found.push({
67
+ path: join(dir, entry, 'SKILL.md'),
68
+ size: stat.size,
69
+ type: 'skill',
70
+ });
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ log(`Could not scan skill dir: ${dir}`);
76
+ }
77
+ }
78
+ }
79
+ log(`Scan complete: found ${found.length} files`);
80
+ return found;
81
+ }
82
+ function readWorkspaceFile(relativePath) {
83
+ // Only allow .md files
84
+ const ext = extname(relativePath).toLowerCase();
85
+ if (!ALLOWED_EXTENSIONS.includes(ext)) {
86
+ log(`BLOCKED non-markdown file: ${relativePath}`);
87
+ throw new Error(`Only .md files are allowed. Got: "${relativePath}"`);
88
+ }
89
+ const fullPath = safePath(relativePath);
90
+ if (!fullPath) {
91
+ throw new Error(`Path not allowed: "${relativePath}" — must be within project directory`);
92
+ }
93
+ if (!existsSync(fullPath)) {
94
+ throw new Error(`File not found: "${relativePath}"`);
95
+ }
96
+ const stat = statSync(fullPath);
97
+ if (!stat.isFile()) {
98
+ throw new Error(`Not a file: "${relativePath}"`);
99
+ }
100
+ let content;
101
+ let truncated = false;
102
+ if (stat.size > MAX_FILE_SIZE) {
103
+ content = readFileSync(fullPath, 'utf-8').slice(0, MAX_FILE_SIZE);
104
+ truncated = true;
105
+ log(`Read ${relativePath} (${stat.size} bytes, truncated to ${MAX_FILE_SIZE})`);
106
+ }
107
+ else {
108
+ content = readFileSync(fullPath, 'utf-8');
109
+ log(`Read ${relativePath} (${stat.size} bytes)`);
110
+ }
111
+ return {
112
+ path: relativePath,
113
+ content,
114
+ size: stat.size,
115
+ truncated,
116
+ };
117
+ }
118
+ function writeWorkspaceFile(relativePath, content) {
119
+ // Only allow .md files
120
+ const ext = extname(relativePath).toLowerCase();
121
+ if (!ALLOWED_EXTENSIONS.includes(ext)) {
122
+ log(`BLOCKED write to non-markdown file: ${relativePath}`);
123
+ throw new Error(`Only .md files are allowed to be written. Got: "${relativePath}"`);
124
+ }
125
+ const fullPath = safePath(relativePath);
126
+ if (!fullPath) {
127
+ throw new Error(`Path not allowed: "${relativePath}" — must be within project directory`);
128
+ }
129
+ // Ensure parent directory exists
130
+ const dir = dirname(fullPath);
131
+ if (!existsSync(dir)) {
132
+ mkdirSync(dir, { recursive: true });
133
+ }
134
+ // We allow creating new files or overwriting existing ones.
135
+ // In a production scenario, you might want to differentiate or append.
136
+ writeFileSync(fullPath, content, 'utf-8');
137
+ log(`Wrote ${relativePath} (${Buffer.byteLength(content, 'utf-8')} bytes)`);
138
+ return {
139
+ path: relativePath,
140
+ size: Buffer.byteLength(content, 'utf-8'),
141
+ message: `Successfully wrote to ${relativePath}`,
142
+ };
143
+ }
144
+ export async function handleWorkspace(input) {
145
+ if (input.mode === 'scan') {
146
+ const files = scanWorkspace();
147
+ return {
148
+ workspace: process.cwd(),
149
+ files,
150
+ hint: 'Use mode "read" with a path from this list to get file contents.',
151
+ };
152
+ }
153
+ if (input.mode === 'read') {
154
+ if (!input.path) {
155
+ throw new Error('path is required when mode is "read"');
156
+ }
157
+ const result = readWorkspaceFile(input.path);
158
+ return result;
159
+ }
160
+ if (input.mode === 'write') {
161
+ if (!input.path) {
162
+ throw new Error('path is required when mode is "write"');
163
+ }
164
+ if (input.content === undefined) {
165
+ throw new Error('content is required when mode is "write"');
166
+ }
167
+ const result = writeWorkspaceFile(input.path, input.content);
168
+ return result;
169
+ }
170
+ throw new Error(`Unknown mode: "${input.mode}". Use "scan", "read", or "write".`);
171
+ }
172
+ //# sourceMappingURL=workspace.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spm-mcp",
3
- "version": "0.3.2",
4
- "description": "Super Product Manager MCP Server - AI-powered product document analysis for PRDs, roadmaps, and 30 PM document types",
3
+ "version": "0.4.1",
4
+ "description": "Super Product Manager MCP Server - AI-powered product document analysis. Bring your own template or use 30 built-in expert reviews for PRDs, roadmaps, and PM documents.",
5
5
  "author": "Super Product Manager <chiranjeevi.gunturi@superproductmanager.ai>",
6
6
  "homepage": "https://superproductmanager.ai",
7
7
  "type": "module",
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import { handleClarify } from './tools/clarify.js';
20
20
  import { handleEvaluate } from './tools/evaluate.js';
21
21
  import { handleImprove } from './tools/improve.js';
22
22
  import { handleCreateCustomNanoApp } from './tools/create-custom-nano-app.js';
23
+ import { handleWorkspace } from './tools/workspace.js';
23
24
  import { SpmApiError } from './client/spm-api.js';
24
25
 
25
26
  function logToolCall(tool: string, input: Record<string, unknown>) {
@@ -47,9 +48,15 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
47
48
  // Tool: spm_list_nano_apps
48
49
  server.tool(
49
50
  'spm_list_nano_apps',
50
- 'List all available SPM nano app templates for product document analysis. ' +
51
- 'Returns template keys, names, descriptions, and categories. ' +
52
- 'Use this to discover which analysis types are available before calling spm_analyze.',
51
+ 'List available SPM nano app templates. ' +
52
+ 'IMPORTANT: Do NOT call this as your first action. Instead:\n' +
53
+ '1. Ask the user what document or task they are working on.\n' +
54
+ '2. Check if they have an existing template, PRD, or evaluation criteria ' +
55
+ '(look in workspace files, Claude project instructions, or ask).\n' +
56
+ '3. If they have their own template or a favorite PRD, use spm_create_custom_nano_app instead.\n' +
57
+ '4. Only call this tool if the user explicitly wants to browse available templates.\n' +
58
+ 'Returns template keys, names, descriptions, and categories. ' +
59
+ 'DISPLAY: Show the top 5-6 most popular first. Only show the full list if the user asks for it.',
53
60
  {},
54
61
  async () => {
55
62
  try {
@@ -68,11 +75,11 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
68
75
  server.tool(
69
76
  'spm_analyze',
70
77
  'Analyze a product document against SPM expert expectations. ' +
71
- 'Pass a document and a nano_app_id (e.g., "prd_critique", "user_story", "growth_strategy") ' +
72
- 'to get an expert analysis with expectations, sub-expectations, scores, and evidence. ' +
73
- 'Call spm_list_nano_apps first if you need to discover available templates. ' +
74
- 'IMPORTANT: Present results as a clear scorecard table (Expectation | Score | Verdict). ' +
75
- 'Highlight critical gaps (score < 50%) and suggest running spm_clarify on the weakest gap next.',
78
+ 'Pass a document and a nano_app_id (e.g., "prd_critique", "user_story", "growth_strategy") ' +
79
+ 'to get an expert analysis with expectations, sub-expectations, scores, and evidence. ' +
80
+ 'Call spm_list_nano_apps first if you need to discover available templates. ' +
81
+ 'IMPORTANT: Present results as a clear scorecard table (Expectation | Score | Verdict). ' +
82
+ 'Highlight critical gaps (score < 50%) and suggest running spm_clarify on the weakest gap next.',
76
83
  {
77
84
  document: z.string().describe('The product document text to analyze'),
78
85
  nano_app_id: z.string().describe(
@@ -96,13 +103,13 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
96
103
  server.tool(
97
104
  'spm_clarify',
98
105
  'Get clarification questions for gaps identified in an SPM analysis. ' +
99
- 'Pass the original document, the expert analysis context, and a target gap. ' +
100
- 'Returns targeted questions with suggested answers to close document gaps. ' +
101
- 'IMPORTANT: When presenting results to the user, minimize cognitive load. ' +
102
- 'Show the question prominently, then present each answer option as a clear ' +
103
- 'numbered choice the user can pick (1, 2, 3...). Mark the [recommended] option. ' +
104
- 'If you have native interactive UI (buttons, selectable options, AskUserQuestion), use it. ' +
105
- 'The user should be able to pick an option or type their own answer with minimal effort.',
106
+ 'Pass the original document, the expert analysis context, and a target gap. ' +
107
+ 'Returns targeted questions with suggested answers to close document gaps. ' +
108
+ 'IMPORTANT: When presenting results to the user, minimize cognitive load. ' +
109
+ 'Show the question prominently, then present each answer option as a clear ' +
110
+ 'numbered choice the user can pick (1, 2, 3...). Mark the [recommended] option. ' +
111
+ 'If you have native interactive UI (buttons, selectable options, AskUserQuestion), use it. ' +
112
+ 'The user should be able to pick an option or type their own answer with minimal effort.',
106
113
  {
107
114
  document: z.string().describe('The original product document text'),
108
115
  nano_app_id: z.string().describe('The nano app template key used in the analysis'),
@@ -143,9 +150,9 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
143
150
  server.tool(
144
151
  'spm_evaluate',
145
152
  'Re-score sub-expectations after clarification rounds. ' +
146
- 'Pass the original document, the expectations from spm_analyze, and all Q&A pairs from spm_clarify. ' +
147
- 'Returns updated scores showing which gaps are now covered (>=0.81) and which still need work. ' +
148
- 'Use after every 3 rounds of spm_clarify to measure progress.',
153
+ 'Pass the original document, the expectations from spm_analyze, and all Q&A pairs from spm_clarify. ' +
154
+ 'Returns updated scores showing which gaps are now covered (>=0.81) and which still need work. ' +
155
+ 'Use after every 3 rounds of spm_clarify to measure progress.',
149
156
  {
150
157
  document: z.string().describe('The original product document text'),
151
158
  nano_app_id: z.string().describe('The nano app template key used in the analysis'),
@@ -183,11 +190,11 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
183
190
  server.tool(
184
191
  'spm_improve',
185
192
  'Generate improved document content for a specific gap. ' +
186
- 'Pass the document, the target sub-expectation, and all clarification Q&A pairs. ' +
187
- 'Returns paste-ready content the PM can insert into their document. ' +
188
- 'Use after spm_evaluate confirms the gap is sufficiently covered by clarification answers. ' +
189
- 'IMPORTANT: Present the generated content in clean markdown the user can copy-paste directly. ' +
190
- 'If there are [ACTION NEEDED] markers, highlight them so the user knows what to fill in.',
193
+ 'Pass the document, the target sub-expectation, and all clarification Q&A pairs. ' +
194
+ 'Returns paste-ready content the PM can insert into their document. ' +
195
+ 'Use after spm_evaluate confirms the gap is sufficiently covered by clarification answers. ' +
196
+ 'IMPORTANT: Present the generated content in clean markdown the user can copy-paste directly. ' +
197
+ 'If there are [ACTION NEEDED] markers, highlight them so the user knows what to fill in.',
191
198
  {
192
199
  document: z.string().describe('The original product document text'),
193
200
  nano_app_id: z.string().describe('The nano app template key used in the analysis'),
@@ -222,20 +229,33 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
222
229
  // Tool: spm_create_custom_nano_app
223
230
  server.tool(
224
231
  'spm_create_custom_nano_app',
225
- 'Create a custom nano app when the user\'s document doesn\'t match any existing nano app ' +
226
- '(call spm_list_nano_apps first to check).\n\n' +
227
- 'BEFORE calling this tool, gather preferences from the user:\n' +
228
- '1. WHAT TO EVALUATE → expectations + rules\n' +
229
- '2. DOMAIN LENS domainContext\n' +
230
- '3. GOOD vs BAD SIGNALS reviewer examples\n' +
231
- '4. CLARIFICATION STYLE clarification examples + instructions\n' +
232
- '5. IMPROVEMENT FORMAT improviser instructions\n\n' +
233
- 'IMPORTANT: Do NOT run a rigid 5-question interview. Extract what you can from the user\'s ' +
234
- 'initial message. Only ask 1-2 clarifying questions for what\'s genuinely missing. ' +
235
- 'Bias toward ACTION over interrogation. The endpoint handles missing preferences gracefully.',
232
+ 'Create a personalized nano app from the user\'s own template, PRD, or evaluation criteria. ' +
233
+ 'This is the PREFERRED path for new users who have their own standards.\n\n' +
234
+ 'The endpoint accepts MULTIPLE input types prepare the best input:\n\n' +
235
+ 'BEST (structured description + preferences):\n' +
236
+ ' description: "Evaluate PRDs with 9 sections: Problem, Goal, Target Users, Solution, ' +
237
+ 'Success Metrics, Timeline, Technical Approach, Risks, Out of Scope"\n' +
238
+ ' preferences: { domain_lens: "...", good_bad_signals: "..." }\n\n' +
239
+ 'GOOD (raw PRD or document pasted as description):\n' +
240
+ ' description: <paste the user\'s actual PRD or template document here>\n' +
241
+ ' The endpoint will extract sections and create generic rules from the structure.\n\n' +
242
+ 'OK (bare description):\n' +
243
+ ' description: "Evaluate product strategy for investment readiness"\n\n' +
244
+ 'INPUT PREPARATION — How to build the best input:\n' +
245
+ '1. If the user has a template/PRD, read it and list its sections explicitly in the description ' +
246
+ '(e.g., "9 sections: Problem, Goal, Target Users..."). This produces the best results.\n' +
247
+ '2. Add preferences.domain_lens with who is reviewing and what matters.\n' +
248
+ '3. Add preferences.good_bad_signals with concrete examples of quality.\n' +
249
+ '4. If the user provides explicit evaluation criteria or rules, pass them in the description — ' +
250
+ 'the endpoint will use them as-is, not reinterpret.\n\n' +
251
+ 'DISPLAY: After creation, show a table: Expectation | Rules | What it checks. ' +
252
+ 'Ask the user to confirm before proceeding to spm_analyze. Do NOT show the scoring rubric — ' +
253
+ 'users see it in action when they analyze a document.',
236
254
  {
237
255
  description: z.string().describe(
238
- 'What kind of document this nano app analyzes and what it should evaluate',
256
+ 'BEST: List sections explicitly ("Evaluate PRDs with 9 sections: Problem, Goal, ..."). ' +
257
+ 'ALSO WORKS: Paste a raw PRD/template document — sections will be auto-extracted. ' +
258
+ 'ALSO WORKS: Short description ("Evaluate product strategy for investment readiness").',
239
259
  ),
240
260
  name: z.string().optional().describe(
241
261
  'Human-readable name for the nano app (auto-generated if omitted)',
@@ -260,6 +280,38 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
260
280
  },
261
281
  );
262
282
 
283
+ // Tool: spm_read_workspace
284
+ server.tool(
285
+ 'spm_read_workspace',
286
+ 'Read or write files from the user\'s local workspace to gather project context or record decisions. ' +
287
+ 'Three modes: (1) "scan" — auto-discover known config files (CLAUDE.md, SKILL.md, README.md)\n' +
288
+ '(2) "read" — read a specific .md file by relative path\n' +
289
+ '(3) "write" — write content to a .md file by relative path (e.g., to record a system-of-record decision summary)\n' +
290
+ 'All paths are restricted to the project directory for security, and ONLY .md files are allowed.',
291
+ {
292
+ mode: z.enum(['scan', 'read', 'write']).describe(
293
+ '"scan" to discover files, "read" to get contents, "write" to save contents',
294
+ ),
295
+ path: z.string().optional().describe(
296
+ 'Relative path to the file (required when mode is "read" or "write"). Must be a .md file.',
297
+ ),
298
+ content: z.string().optional().describe(
299
+ 'The markdown content to write to the file (required when mode is "write").',
300
+ ),
301
+ },
302
+ async (input) => {
303
+ try {
304
+ logToolCall('spm_read_workspace', input);
305
+ const result = await handleWorkspace(input);
306
+ return {
307
+ content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
308
+ };
309
+ } catch (err) {
310
+ return errorResponse(err);
311
+ }
312
+ },
313
+ );
314
+
263
315
  return server;
264
316
  }
265
317
 
package/src/stdio.ts CHANGED
@@ -20,7 +20,7 @@ import { createInterface } from 'readline';
20
20
 
21
21
  const CONFIG_DIR = join(homedir(), '.spm');
22
22
  const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
23
- const SETUP_URL = 'https://superproductmanager.web.app/#/profile';
23
+ const SETUP_URL = 'https://superproductmanager.web.app/#/profile?utm_source=npm&utm_medium=cli_setup&utm_campaign=mcp';
24
24
 
25
25
  function isInteractive(): boolean {
26
26
  return process.stdin.isTTY === true;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * spm_read_workspace — Local workspace file reader.
3
+ *
4
+ * Reads files from the user's workspace (cwd) to provide project context.
5
+ * Two modes:
6
+ * - scan: auto-discover known config/doc files and return a manifest
7
+ * - read: read a specific file by relative path
8
+ *
9
+ * Security:
10
+ * - All paths resolved and validated to stay within cwd
11
+ * - Only .md files are allowed (no .env, .json, source code, etc.)
12
+ */
13
+
14
+ import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
15
+ import { resolve, relative, join, basename, extname, dirname } from 'path';
16
+
17
+ const log = (msg: string) => process.stderr.write(`[spm-mcp:workspace] ${msg}\n`);
18
+
19
+ /** Only markdown files are allowed */
20
+ const ALLOWED_EXTENSIONS = ['.md'];
21
+
22
+ /** Files to auto-discover in scan mode */
23
+ const KNOWN_FILES = [
24
+ 'CLAUDE.md',
25
+ 'claude.md',
26
+ 'README.md',
27
+ '.github/copilot-instructions.md',
28
+ ];
29
+
30
+ /** Directories to scan for skill files */
31
+ const SKILL_DIRS = [
32
+ '.claude/skills',
33
+ '.gemini/skills',
34
+ ];
35
+
36
+ /** Max file size to read (100 KB) — skip huge files */
37
+ const MAX_FILE_SIZE = 100 * 1024;
38
+
39
+ // ─── Security ───────────────────────────────────────────────────────
40
+
41
+ function safePath(relativePath: string): string | null {
42
+ const cwd = process.cwd();
43
+ const resolved = resolve(cwd, relativePath);
44
+ const rel = relative(cwd, resolved);
45
+ if (rel.startsWith('..') || rel === resolved) {
46
+ log(`BLOCKED path traversal: ${relativePath}`);
47
+ return null;
48
+ }
49
+ return resolved;
50
+ }
51
+
52
+ // ─── Scan Mode ──────────────────────────────────────────────────────
53
+
54
+ interface FileEntry {
55
+ path: string;
56
+ size: number;
57
+ type: 'config' | 'skill' | 'doc';
58
+ }
59
+
60
+ function scanWorkspace(): FileEntry[] {
61
+ const cwd = process.cwd();
62
+ const found: FileEntry[] = [];
63
+
64
+ // Check known files
65
+ for (const file of KNOWN_FILES) {
66
+ const full = join(cwd, file);
67
+ if (existsSync(full)) {
68
+ const stat = statSync(full);
69
+ if (stat.isFile()) {
70
+ found.push({ path: file, size: stat.size, type: 'config' });
71
+ }
72
+ }
73
+ }
74
+
75
+ // Scan skill directories for SKILL.md files
76
+ for (const dir of SKILL_DIRS) {
77
+ const fullDir = join(cwd, dir);
78
+ if (existsSync(fullDir) && statSync(fullDir).isDirectory()) {
79
+ try {
80
+ const entries = readdirSync(fullDir);
81
+ for (const entry of entries) {
82
+ const skillPath = join(fullDir, entry, 'SKILL.md');
83
+ if (existsSync(skillPath)) {
84
+ const stat = statSync(skillPath);
85
+ found.push({
86
+ path: join(dir, entry, 'SKILL.md'),
87
+ size: stat.size,
88
+ type: 'skill',
89
+ });
90
+ }
91
+ }
92
+ } catch {
93
+ log(`Could not scan skill dir: ${dir}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ log(`Scan complete: found ${found.length} files`);
99
+ return found;
100
+ }
101
+
102
+ // ─── Read Mode ──────────────────────────────────────────────────────
103
+
104
+ interface ReadResult {
105
+ path: string;
106
+ content: string;
107
+ size: number;
108
+ truncated: boolean;
109
+ }
110
+
111
+ function readWorkspaceFile(relativePath: string): ReadResult {
112
+ // Only allow .md files
113
+ const ext = extname(relativePath).toLowerCase();
114
+ if (!ALLOWED_EXTENSIONS.includes(ext)) {
115
+ log(`BLOCKED non-markdown file: ${relativePath}`);
116
+ throw new Error(`Only .md files are allowed. Got: "${relativePath}"`);
117
+ }
118
+
119
+ const fullPath = safePath(relativePath);
120
+ if (!fullPath) {
121
+ throw new Error(`Path not allowed: "${relativePath}" — must be within project directory`);
122
+ }
123
+
124
+ if (!existsSync(fullPath)) {
125
+ throw new Error(`File not found: "${relativePath}"`);
126
+ }
127
+
128
+ const stat = statSync(fullPath);
129
+ if (!stat.isFile()) {
130
+ throw new Error(`Not a file: "${relativePath}"`);
131
+ }
132
+
133
+ let content: string;
134
+ let truncated = false;
135
+
136
+ if (stat.size > MAX_FILE_SIZE) {
137
+ content = readFileSync(fullPath, 'utf-8').slice(0, MAX_FILE_SIZE);
138
+ truncated = true;
139
+ log(`Read ${relativePath} (${stat.size} bytes, truncated to ${MAX_FILE_SIZE})`);
140
+ } else {
141
+ content = readFileSync(fullPath, 'utf-8');
142
+ log(`Read ${relativePath} (${stat.size} bytes)`);
143
+ }
144
+
145
+ return {
146
+ path: relativePath,
147
+ content,
148
+ size: stat.size,
149
+ truncated,
150
+ };
151
+ }
152
+
153
+ // ─── Write Mode ─────────────────────────────────────────────────────
154
+
155
+ interface WriteResult {
156
+ path: string;
157
+ size: number;
158
+ message: string;
159
+ }
160
+
161
+ function writeWorkspaceFile(relativePath: string, content: string): WriteResult {
162
+ // Only allow .md files
163
+ const ext = extname(relativePath).toLowerCase();
164
+ if (!ALLOWED_EXTENSIONS.includes(ext)) {
165
+ log(`BLOCKED write to non-markdown file: ${relativePath}`);
166
+ throw new Error(`Only .md files are allowed to be written. Got: "${relativePath}"`);
167
+ }
168
+
169
+ const fullPath = safePath(relativePath);
170
+ if (!fullPath) {
171
+ throw new Error(`Path not allowed: "${relativePath}" — must be within project directory`);
172
+ }
173
+
174
+ // Ensure parent directory exists
175
+ const dir = dirname(fullPath);
176
+ if (!existsSync(dir)) {
177
+ mkdirSync(dir, { recursive: true });
178
+ }
179
+
180
+ // We allow creating new files or overwriting existing ones.
181
+ // In a production scenario, you might want to differentiate or append.
182
+ writeFileSync(fullPath, content, 'utf-8');
183
+
184
+ log(`Wrote ${relativePath} (${Buffer.byteLength(content, 'utf-8')} bytes)`);
185
+
186
+ return {
187
+ path: relativePath,
188
+ size: Buffer.byteLength(content, 'utf-8'),
189
+ message: `Successfully wrote to ${relativePath}`,
190
+ };
191
+ }
192
+
193
+ // ─── Exported Handler ───────────────────────────────────────────────
194
+
195
+ export interface WorkspaceInput {
196
+ mode: 'scan' | 'read' | 'write';
197
+ path?: string;
198
+ content?: string;
199
+ }
200
+
201
+ export async function handleWorkspace(input: WorkspaceInput): Promise<unknown> {
202
+ if (input.mode === 'scan') {
203
+ const files = scanWorkspace();
204
+ return {
205
+ workspace: process.cwd(),
206
+ files,
207
+ hint: 'Use mode "read" with a path from this list to get file contents.',
208
+ };
209
+ }
210
+
211
+ if (input.mode === 'read') {
212
+ if (!input.path) {
213
+ throw new Error('path is required when mode is "read"');
214
+ }
215
+ const result = readWorkspaceFile(input.path);
216
+ return result;
217
+ }
218
+
219
+ if (input.mode === 'write') {
220
+ if (!input.path) {
221
+ throw new Error('path is required when mode is "write"');
222
+ }
223
+ if (input.content === undefined) {
224
+ throw new Error('content is required when mode is "write"');
225
+ }
226
+ const result = writeWorkspaceFile(input.path, input.content);
227
+ return result;
228
+ }
229
+
230
+ throw new Error(`Unknown mode: "${input.mode}". Use "scan", "read", or "write".`);
231
+ }
@@ -0,0 +1,44 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { unlinkSync } from "fs";
4
+
5
+ async function run() {
6
+ const transport = new StdioClientTransport({
7
+ command: "node",
8
+ args: ["./dist/src/stdio.js"]
9
+ });
10
+
11
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
12
+ await client.connect(transport);
13
+
14
+ console.log("Connected to MCP Server via stdio!");
15
+
16
+ try {
17
+ console.log("\nCalling spm_read_workspace (mode: write)...");
18
+ const result = await client.callTool({
19
+ name: "spm_read_workspace",
20
+ arguments: {
21
+ mode: "write",
22
+ path: "mcp_protocol_test.md",
23
+ content: "# Real MCP Test\n\nThis was written via the actual MCP JSON-RPC protocol!"
24
+ }
25
+ });
26
+ console.log("Tool Result:", JSON.stringify(result, null, 2));
27
+
28
+ // read it back
29
+ console.log("\nCalling spm_read_workspace (mode: read)...");
30
+ const readResult = await client.callTool({
31
+ name: "spm_read_workspace",
32
+ arguments: { mode: "read", path: "mcp_protocol_test.md" }
33
+ });
34
+ console.log("Read Result text:\n" + readResult.content[0].text);
35
+
36
+ } catch (err) {
37
+ console.error("Error:", err);
38
+ } finally {
39
+ try { unlinkSync('mcp_protocol_test.md'); } catch (e) { }
40
+ console.log("\nCleanup done. Test complete.");
41
+ process.exit(0);
42
+ }
43
+ }
44
+ run();