spm-mcp 0.1.0 → 0.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # spm-mcp
2
2
 
3
- **Cursor for product management** expert document review as MCP tools.
3
+ **Cursor for product management.** Expert document review as MCP tools.
4
4
 
5
- AI made engineering 10x faster. But deciding *what* to build didn't get faster. SPM fixes the input 30 domain-specific expert reviews that score your product documents against the standards senior PMs actually use.
5
+ AI made engineering 10x faster. But deciding *what* to build didn't get faster. SPM fixes the input with 30 domain-specific expert reviews that score your product documents against the standards senior PMs actually use.
6
6
 
7
7
  ```bash
8
8
  npx spm-mcp
@@ -10,59 +10,74 @@ npx spm-mcp
10
10
 
11
11
  ## The problem
12
12
 
13
- AI agents build what you specify including every blind spot. A vague spec used to waste a sprint. Now it ships to production in hours. SPM catches the blind spots before they become shipped code.
13
+ AI agents build what you specify, including every blind spot. A vague spec used to waste a sprint. Now it ships to production in hours. SPM catches the blind spots before they become shipped code.
14
14
 
15
15
  ## What SPM reviews
16
16
 
17
17
  Not just PRDs. Every document in the PM lifecycle:
18
18
 
19
- - **Strategy** Product Roadmap, Growth Strategy, Go-to-Market, OKR Planning, Product Vision
20
- - **Analysis** Competitive Analysis, Market Research, Product Metrics, Stakeholder Management
21
- - **Execution** User Stories, Feature Spec, Sprint Planning, PRD to Jira, Release Notes, Test Cases
22
- - **Discovery** Problem Statement, Persona, Jobs-to-be-Done, Opportunity Assessment
19
+ - **Strategy:** Product Roadmap, Growth Strategy, Go-to-Market, OKR Planning, Product Vision
20
+ - **Analysis:** Competitive Analysis, Market Research, Product Metrics, Stakeholder Management
21
+ - **Execution:** User Stories, Feature Spec, Sprint Planning, PRD to Jira, Release Notes, Test Cases
22
+ - **Discovery:** Problem Statement, Persona, Jobs-to-be-Done, Opportunity Assessment
23
23
 
24
24
  30 built-in expert reviews. Or create your own with `spm_create_custom_nano_app`.
25
25
 
26
26
  ## Quick start
27
27
 
28
- ### 1. Get your API key
29
-
30
- Sign in at [superproductmanager.ai](https://superproductmanager.ai) → Profile → Generate API Key
31
-
32
- ### 2. Set it
28
+ ### Option A: Guided setup (recommended)
33
29
 
34
30
  ```bash
35
- export SPM_API_KEY=spm_k_your_key_here
31
+ npx spm-mcp
36
32
  ```
37
33
 
38
- ### 3. Run
34
+ First run auto-detects no API key and walks you through setup:
35
+ 1. Opens your browser to sign in
36
+ 2. You generate an API key on your Profile page
37
+ 3. Paste it back in the terminal
38
+ 4. Key saved to `~/.spm/config.json`. Done.
39
+
40
+ ### Option B: Manual setup
39
41
 
40
42
  ```bash
43
+ # 1. Get your key at superproductmanager.ai > Profile > Generate API Key
44
+ # 2. Set it
45
+ export SPM_API_KEY=spm_k_your_key_here
46
+ # 3. Run
41
47
  npx spm-mcp
42
48
  ```
43
49
 
50
+ ### Option C: claude.ai (remote MCP)
51
+
52
+ For claude.ai web users, SPM is available as a remote MCP server:
53
+
54
+ 1. Get your API key at [superproductmanager.ai](https://superproductmanager.ai) > Profile
55
+ 2. In claude.ai, go to Settings > Integrations > Add MCP Server
56
+ 3. URL: `https://spm-mcp.superproductmanager.ai/mcp`
57
+ 4. Add header: `X-SPM-API-Key: spm_k_your_key_here`
58
+
44
59
  ## Tools
45
60
 
46
61
  | Tool | What it does |
47
62
  |------|-------------|
48
63
  | `spm_list_nano_apps` | Discover available expert reviews (30 built-in + your custom ones) |
49
- | `spm_analyze` | Score a document against domain-specific expert expectations. Every gap scored 01.0 with evidence. |
64
+ | `spm_analyze` | Score a document against domain-specific expert expectations. Every gap scored 0-1.0 with evidence. |
50
65
  | `spm_clarify` | Get decision-forcing questions for the weakest gaps. Questions escalate when you give vague answers. |
51
66
  | `spm_evaluate` | Re-score gaps after clarification rounds. Tracks progress. Use after every 3 rounds of `spm_clarify`. |
52
- | `spm_improve` | Generate paste-ready improvements grounded in your answers not AI hallucination. |
67
+ | `spm_improve` | Generate paste-ready improvements grounded in your answers, not AI hallucination. |
53
68
  | `spm_create_custom_nano_app` | Create a custom expert review for any document type not covered by the built-in 30. |
54
69
 
55
70
  ## How it works
56
71
 
57
72
  ```
58
73
  Your document
59
- spm_analyze Scores every gap 01.0 against expert expectations
60
- spm_clarify Asks the questions your stakeholder would ask
61
- spm_evaluate Re-scores did the gap close?
62
- spm_improve Generates improvements from YOUR answers
74
+ > spm_analyze Scores every gap 0-1.0 against expert expectations
75
+ > spm_clarify Asks the questions your stakeholder would ask
76
+ > spm_evaluate Re-scores. Did the gap close?
77
+ > spm_improve Generates improvements from YOUR answers
63
78
  ```
64
79
 
65
- 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.
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.
66
81
 
67
82
  ## Example workflow
68
83
 
@@ -73,8 +88,8 @@ SPM: 4 expectations scored. Problem Definition: 43/100. Success Metrics: 15/1
73
88
  You: Clarify the weakest gap
74
89
  SPM: "Is operator churn or player registration drop-off your primary counter-metric?"
75
90
 
76
- You: Operator churn if >20% revert to WhatsApp, we've failed.
77
- SPM: [re-evaluates] Counter-metric: 0 0.9. Now targeting: Instrumentation plan.
91
+ You: Operator churn. If >20% revert to WhatsApp, we've failed.
92
+ SPM: [re-evaluates] Counter-metric: 0 > 0.9. Now targeting: Instrumentation plan.
78
93
 
79
94
  You: Improve the success metrics section
80
95
  SPM: [generates paste-ready content grounded in your "operator churn" decision]
@@ -86,17 +101,18 @@ Average documents improve from 35% to 82% in three rounds.
86
101
 
87
102
  ### Claude Code
88
103
 
89
- Add to `.claude/settings.json`:
104
+ ```bash
105
+ claude mcp add spm -- npx spm-mcp
106
+ ```
107
+
108
+ Or add to `.claude/settings.json`. If you ran `npx spm-mcp` setup already, the key is in `~/.spm/config.json` and no env var is needed:
90
109
 
91
110
  ```json
92
111
  {
93
112
  "mcpServers": {
94
113
  "spm": {
95
114
  "command": "npx",
96
- "args": ["spm-mcp"],
97
- "env": {
98
- "SPM_API_KEY": "spm_k_your_key_here"
99
- }
115
+ "args": ["spm-mcp"]
100
116
  }
101
117
  }
102
118
  }
@@ -126,30 +142,31 @@ Add to MCP configuration with the same command and env structure.
126
142
 
127
143
  | Variable | Required | Default | Description |
128
144
  |----------|----------|---------|-------------|
129
- | `SPM_API_KEY` | Yes | | Your API key from [superproductmanager.ai](https://superproductmanager.ai) Profile |
130
- | `SPM_SUPABASE_URL` | No | Production | Override for self-hosted or development |
131
- | `SPM_SUPABASE_ANON_KEY` | No | Production | Override for self-hosted or development |
145
+ | `SPM_API_KEY` | Only if no `~/.spm/config.json` | n/a | Your API key. Run `npx spm-mcp` for guided setup. |
146
+ | `SPM_SUPABASE_URL` | No | Production | Override for development |
147
+ | `SPM_SUPABASE_ANON_KEY` | No | Production | Override for development |
132
148
  | `SPM_CLOUD_FUNCTIONS_BASE` | No | Production | Override Cloud Functions URL |
133
149
 
150
+ API key resolution order: `SPM_API_KEY` env var > `~/.spm/config.json` > setup prompt.
151
+
134
152
  ## Security
135
153
 
136
- - **No filesystem access** SPM never reads your local files, SSH keys, or credentials
137
- - **No postinstall scripts** nothing runs on `npm install`
138
- - **4 specific env vars only** reads `SPM_API_KEY`, `SPM_SUPABASE_URL`, `SPM_SUPABASE_ANON_KEY`, `SPM_CLOUD_FUNCTIONS_BASE`. No broad `process.env` access.
139
- - **Network calls only when tools are invoked** not on import, not on install
140
- - **Source included** TypeScript source ships alongside compiled JS for auditability
141
- - **Zero known vulnerabilities** `npm audit` clean
154
+ - **No filesystem access.** SPM never reads your local files, SSH keys, or credentials.
155
+ - **No postinstall scripts.** Nothing runs on `npm install`.
156
+ - **Minimal env access.** Reads 4 optional env vars and `~/.spm/config.json`. No broad `process.env` access.
157
+ - **Network calls only when tools are invoked.** Not on import, not on install.
158
+ - **Source included.** TypeScript source ships alongside compiled JS for auditability.
159
+ - **Zero known vulnerabilities.** `npm audit` clean.
142
160
 
143
161
  ## Also available as
144
162
 
145
- - **Chrome extension** reviews documents inside Google Docs, Notion, ClickUp, Linear. [Install from Chrome Web Store](https://chromewebstore.google.com/detail/super-product-manager/ocpjfedoogmpbkhpojdkfdimkbiamihg)
146
- - **Web app** paste any document, full analysis in 30 seconds. [superproductmanager.ai](https://superproductmanager.ai)
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)
147
165
 
148
166
  ## Links
149
167
 
150
168
  - [Website](https://superproductmanager.ai)
151
169
  - [Chrome Extension](https://chromewebstore.google.com/detail/super-product-manager/ocpjfedoogmpbkhpojdkfdimkbiamihg)
152
- - [Y Combinator RFS: "Cursor for Product Management"](https://www.ycombinator.com/rfs)
153
170
 
154
171
  ## License
155
172
 
@@ -2,4 +2,5 @@
2
2
  * Helper to call Cloud Functions that return plain JSON (not SSE).
3
3
  * Used for nano app CRUD and discovery endpoints.
4
4
  */
5
+ export declare function setChannel(ch: string): void;
5
6
  export declare function callJsonEndpoint(endpoint: string, body?: Record<string, unknown>, apiKey?: string): Promise<unknown>;
@@ -3,10 +3,15 @@
3
3
  * Used for nano app CRUD and discovery endpoints.
4
4
  */
5
5
  import { config } from '../config.js';
6
+ // Channel identifier — overridden by hosted/cloud-run.ts
7
+ let _channel = 'mcp-npm';
8
+ export function setChannel(ch) { _channel = ch; }
6
9
  export async function callJsonEndpoint(endpoint, body = {}, apiKey) {
7
10
  const url = `${config.cloudFunctionsBase}/${endpoint}`;
8
11
  const headers = {
9
12
  'Content-Type': 'application/json',
13
+ 'Origin': 'https://mcp.superproductmanager.ai',
14
+ 'X-SPM-Channel': _channel,
10
15
  };
11
16
  if (apiKey) {
12
17
  headers['X-SPM-API-Key'] = apiKey;
@@ -13,6 +13,7 @@ declare const ENDPOINTS: {
13
13
  readonly classifyIntent: "/classifyIntent";
14
14
  };
15
15
  type EndpointName = keyof typeof ENDPOINTS;
16
+ export declare function setChannel(ch: string): void;
16
17
  /**
17
18
  * Call an SPM Cloud Function endpoint.
18
19
  * Handles SSE stream parsing automatically.
@@ -14,11 +14,16 @@ const ENDPOINTS = {
14
14
  extensionReviewAgainAgentV3: '/extensionReviewAgainAgentV3',
15
15
  classifyIntent: '/classifyIntent',
16
16
  };
17
+ // Channel identifier — shared with cloud-functions.ts
18
+ let _channel = 'mcp-npm';
19
+ export function setChannel(ch) { _channel = ch; }
17
20
  function getHeaders(apiKey) {
18
21
  const key = apiKey || config.apiKey;
19
22
  const headers = {
20
23
  'Content-Type': 'application/json',
21
24
  'Accept': 'text/event-stream, application/json',
25
+ 'Origin': 'https://mcp.superproductmanager.ai',
26
+ 'X-SPM-Channel': _channel,
22
27
  };
23
28
  if (key) {
24
29
  headers['X-SPM-API-Key'] = key;
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * SPM MCP Server configuration.
3
- * All credentials must be provided via environment variables.
3
+ *
4
+ * Only SPM_API_KEY is required from the user.
5
+ * Supabase values are public (protected by RLS) and default to production.
6
+ * API key is the security boundary: user-specific, hashed, revocable.
4
7
  */
5
8
  export declare const config: {
6
- /** Supabase project URL */
9
+ /** Supabase project URL (public, RLS-protected) */
7
10
  readonly supabaseUrl: string;
8
- /** Supabase anon key */
11
+ /** Supabase anon key (public, RLS-protected) */
9
12
  readonly supabaseAnonKey: string;
10
13
  /** Base URL for Cloud Functions */
11
14
  readonly cloudFunctionsBase: string;
@@ -1,22 +1,43 @@
1
1
  /**
2
2
  * SPM MCP Server configuration.
3
- * All credentials must be provided via environment variables.
3
+ *
4
+ * Only SPM_API_KEY is required from the user.
5
+ * Supabase values are public (protected by RLS) and default to production.
6
+ * API key is the security boundary: user-specific, hashed, revocable.
4
7
  */
5
- function requireEnv(name) {
6
- const v = process.env[name];
7
- if (!v)
8
- throw new Error(`${name} environment variable is required`);
9
- return v;
8
+ import { readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ // ─── Public defaults (same values shipped in client-side bundle) ────
12
+ const DEFAULT_SUPABASE_URL = 'https://zrqngfkafphexqrteukm.supabase.co';
13
+ const DEFAULT_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpycW5nZmthZnBoZXhxcnRldWttIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTYyNjc2MTIsImV4cCI6MjAzMTg0MzYxMn0.YNeVJnNwoTRKPRUi3JT0JQXq8XGXZ1GI6q1sqL9l0b4';
14
+ const DEFAULT_CLOUD_FUNCTIONS_BASE = 'https://us-central1-litethink.cloudfunctions.net';
15
+ // ─── Config file persistence (~/.spm/config.json) ───────────────────
16
+ function loadConfigFile() {
17
+ try {
18
+ const configPath = join(homedir(), '.spm', 'config.json');
19
+ const raw = readFileSync(configPath, 'utf8');
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return {};
24
+ }
25
+ }
26
+ const savedConfig = loadConfigFile();
27
+ // ─── Resolve API key: env var > config file ─────────────────────────
28
+ function resolveApiKey() {
29
+ const key = process.env.SPM_API_KEY || savedConfig.apiKey || '';
30
+ return key;
10
31
  }
11
32
  export const config = {
12
- /** Supabase project URL */
13
- supabaseUrl: requireEnv('SPM_SUPABASE_URL'),
14
- /** Supabase anon key */
15
- supabaseAnonKey: requireEnv('SPM_SUPABASE_ANON_KEY'),
33
+ /** Supabase project URL (public, RLS-protected) */
34
+ supabaseUrl: process.env.SPM_SUPABASE_URL || DEFAULT_SUPABASE_URL,
35
+ /** Supabase anon key (public, RLS-protected) */
36
+ supabaseAnonKey: process.env.SPM_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY,
16
37
  /** Base URL for Cloud Functions */
17
- cloudFunctionsBase: process.env.SPM_CLOUD_FUNCTIONS_BASE || 'https://us-central1-litethink.cloudfunctions.net',
38
+ cloudFunctionsBase: process.env.SPM_CLOUD_FUNCTIONS_BASE || DEFAULT_CLOUD_FUNCTIONS_BASE,
18
39
  /** User's SPM API key for authenticating with Cloud Functions */
19
- apiKey: process.env.SPM_API_KEY || '',
40
+ apiKey: resolveApiKey(),
20
41
  /** Port for hosted HTTP transport */
21
42
  port: parseInt(process.env.PORT || '8080', 10),
22
43
  };
package/dist/src/index.js CHANGED
@@ -71,6 +71,8 @@ export function createSpmMcpServer(options) {
71
71
  question: z.string(),
72
72
  answer: z.string(),
73
73
  })).optional().describe('Array of previous Q&A pairs from this clarification session. Empty for first question.'),
74
+ user_context: z.string().optional().describe('Context about the PM: role, experience level, company stage, research done, decisions made. Helps generate tailored questions.'),
75
+ about_company: z.string().optional().describe('Context about the company/product: stage, team size, market, existing users, business model.'),
74
76
  }, async (input) => {
75
77
  try {
76
78
  const result = await handleClarify(input, apiKey);
@@ -95,6 +97,8 @@ export function createSpmMcpServer(options) {
95
97
  question: z.string(),
96
98
  answer: z.string(),
97
99
  })).describe('All Q&A pairs accumulated from spm_clarify rounds. The evaluator uses these as evidence.'),
100
+ user_context: z.string().optional().describe('Context about the PM: role, experience level, company stage, research done, decisions made.'),
101
+ about_company: z.string().optional().describe('Context about the company/product: stage, team size, market, existing users, business model.'),
98
102
  }, async (input) => {
99
103
  try {
100
104
  const result = await handleEvaluate(input, apiKey);
@@ -2,18 +2,11 @@
2
2
  /**
3
3
  * Stdio transport entry point for the SPM MCP Server.
4
4
  *
5
- * Used by Claude Code and other local MCP clients that spawn the server
6
- * as a subprocess. API key is read from SPM_API_KEY env var.
5
+ * Two modes:
6
+ * 1. Setup (interactive): `npx spm-mcp --setup` or auto-detected on first run
7
+ * Opens browser for login, prompts for API key, saves to ~/.spm/config.json
7
8
  *
8
- * Configure in .claude/settings.json:
9
- * {
10
- * "mcpServers": {
11
- * "spm": {
12
- * "command": "node",
13
- * "args": ["/path/to/packages/mcp-server/dist/src/stdio.js"],
14
- * "env": { "SPM_API_KEY": "spm_k_..." }
15
- * }
16
- * }
17
- * }
9
+ * 2. Server (non-interactive): started by Claude Code / Cursor as MCP subprocess
10
+ * Reads API key from ~/.spm/config.json or SPM_API_KEY env var
18
11
  */
19
12
  export {};
package/dist/src/stdio.js CHANGED
@@ -2,23 +2,107 @@
2
2
  /**
3
3
  * Stdio transport entry point for the SPM MCP Server.
4
4
  *
5
- * Used by Claude Code and other local MCP clients that spawn the server
6
- * as a subprocess. API key is read from SPM_API_KEY env var.
5
+ * Two modes:
6
+ * 1. Setup (interactive): `npx spm-mcp --setup` or auto-detected on first run
7
+ * Opens browser for login, prompts for API key, saves to ~/.spm/config.json
7
8
  *
8
- * Configure in .claude/settings.json:
9
- * {
10
- * "mcpServers": {
11
- * "spm": {
12
- * "command": "node",
13
- * "args": ["/path/to/packages/mcp-server/dist/src/stdio.js"],
14
- * "env": { "SPM_API_KEY": "spm_k_..." }
15
- * }
16
- * }
17
- * }
9
+ * 2. Server (non-interactive): started by Claude Code / Cursor as MCP subprocess
10
+ * Reads API key from ~/.spm/config.json or SPM_API_KEY env var
18
11
  */
19
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
13
  import { createSpmMcpServer } from './index.js';
21
- const server = createSpmMcpServer();
22
- const transport = new StdioServerTransport();
23
- await server.connect(transport);
14
+ import { config } from './config.js';
15
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { homedir } from 'os';
18
+ import { createInterface } from 'readline';
19
+ const CONFIG_DIR = join(homedir(), '.spm');
20
+ const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
21
+ const SETUP_URL = 'https://superproductmanager.web.app/#/profile';
22
+ function isInteractive() {
23
+ return process.stdin.isTTY === true;
24
+ }
25
+ function hasApiKey() {
26
+ return config.apiKey !== '' && config.apiKey.startsWith('spm_k_');
27
+ }
28
+ async function prompt(question) {
29
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
30
+ return new Promise((resolve) => {
31
+ rl.question(question, (answer) => {
32
+ rl.close();
33
+ resolve(answer.trim());
34
+ });
35
+ });
36
+ }
37
+ function saveApiKey(key) {
38
+ let existing = {};
39
+ try {
40
+ existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
41
+ }
42
+ catch { /* no existing config */ }
43
+ if (!existsSync(CONFIG_DIR)) {
44
+ mkdirSync(CONFIG_DIR, { recursive: true });
45
+ }
46
+ writeFileSync(CONFIG_PATH, JSON.stringify({ ...existing, apiKey: key }, null, 2) + '\n');
47
+ }
48
+ async function runSetup() {
49
+ const log = (msg) => process.stderr.write(msg + '\n');
50
+ log('');
51
+ log(' Super Product Manager (SPM) MCP Server');
52
+ log(' ───────────────────────────────────────');
53
+ log('');
54
+ log(' No API key found. Let\'s set you up.');
55
+ log('');
56
+ log(` 1. Sign in at: ${SETUP_URL}`);
57
+ log(' 2. Go to Profile > Generate API Key');
58
+ log(' 3. Copy the key (starts with spm_k_)');
59
+ log('');
60
+ // Try to open browser
61
+ try {
62
+ const { exec } = await import('child_process');
63
+ const cmd = process.platform === 'darwin' ? 'open' :
64
+ process.platform === 'win32' ? 'start' : 'xdg-open';
65
+ exec(`${cmd} "${SETUP_URL}"`);
66
+ log(' Browser opened. Sign in and generate your key.');
67
+ }
68
+ catch {
69
+ log(` Open this URL in your browser: ${SETUP_URL}`);
70
+ }
71
+ log('');
72
+ const key = await prompt(' Paste your API key here: ');
73
+ if (!key.startsWith('spm_k_')) {
74
+ log('');
75
+ log(' Invalid key. API keys start with spm_k_');
76
+ log(' Run `npx spm-mcp --setup` to try again.');
77
+ return false;
78
+ }
79
+ saveApiKey(key);
80
+ log('');
81
+ log(` Key saved to ${CONFIG_PATH}`);
82
+ log('');
83
+ log(' You\'re all set! Add SPM to your AI tool:');
84
+ log('');
85
+ log(' Claude Code: claude mcp add spm -- npx spm-mcp');
86
+ log(' Cursor: Add to .cursor/mcp.json');
87
+ log('');
88
+ return true;
89
+ }
90
+ // ─── Main ───────────────────────────────────────────────────────────
91
+ const wantsSetup = process.argv.includes('--setup');
92
+ if (wantsSetup || (isInteractive() && !hasApiKey())) {
93
+ // Interactive setup mode
94
+ const ok = await runSetup();
95
+ process.exit(ok ? 0 : 1);
96
+ }
97
+ else if (!hasApiKey() && isInteractive()) {
98
+ // No key and interactive — guide the user
99
+ process.stderr.write('\n No SPM_API_KEY found. Run: npx spm-mcp --setup\n\n');
100
+ process.exit(1);
101
+ }
102
+ else {
103
+ // MCP server mode (non-interactive, started by AI tool)
104
+ const server = createSpmMcpServer({ apiKey: config.apiKey });
105
+ const transport = new StdioServerTransport();
106
+ await server.connect(transport);
107
+ }
24
108
  //# sourceMappingURL=stdio.js.map
@@ -47,6 +47,14 @@ export declare const clarifyDefinition: {
47
47
  };
48
48
  description: string;
49
49
  };
50
+ user_context: {
51
+ type: string;
52
+ description: string;
53
+ };
54
+ about_company: {
55
+ type: string;
56
+ description: string;
57
+ };
50
58
  };
51
59
  required: string[];
52
60
  };
@@ -61,5 +69,7 @@ export interface ClarifyInput {
61
69
  question: string;
62
70
  answer: string;
63
71
  }>;
72
+ user_context?: string;
73
+ about_company?: string;
64
74
  }
65
75
  export declare function handleClarify(input: ClarifyInput, apiKey?: string): Promise<unknown>;
@@ -52,12 +52,23 @@ export const clarifyDefinition = {
52
52
  description: 'Array of previous Q&A pairs from this clarification session. ' +
53
53
  'Pass as [{question, answer}, ...]. Empty array for first question.',
54
54
  },
55
+ user_context: {
56
+ type: 'string',
57
+ description: 'Context about the PM: their role, experience level, company stage, ' +
58
+ 'what research they have done, what decisions they have already made. ' +
59
+ 'This helps generate questions tailored to the specific user rather than generic ones.',
60
+ },
61
+ about_company: {
62
+ type: 'string',
63
+ description: 'Context about the company/product: stage, team size, market, ' +
64
+ 'existing users, business model. Helps ground questions in reality.',
65
+ },
55
66
  },
56
67
  required: ['document', 'nano_app_id', 'main_expectation', 'all_sub_expectations', 'target_sub_expectation'],
57
68
  },
58
69
  };
59
70
  export async function handleClarify(input, apiKey) {
60
- const { document, nano_app_id, main_expectation, all_sub_expectations, target_sub_expectation, previous_answers = [], } = input;
71
+ const { document, nano_app_id, main_expectation, all_sub_expectations, target_sub_expectation, previous_answers = [], user_context = '', about_company = '', } = input;
61
72
  if (!document?.trim()) {
62
73
  throw new SpmApiError('Document text is required', 'INVALID_INPUT');
63
74
  }
@@ -88,6 +99,8 @@ export async function handleClarify(input, apiKey) {
88
99
  recent_recommendation: '',
89
100
  latest_review: '',
90
101
  gap_coverage: '',
102
+ user_context,
103
+ about_company,
91
104
  nano_app_id,
92
105
  };
93
106
  const result = await callClarificationAgent(payload, apiKey);
@@ -17,5 +17,7 @@ export interface EvaluateInput {
17
17
  question: string;
18
18
  answer: string;
19
19
  }>;
20
+ user_context?: string;
21
+ about_company?: string;
20
22
  }
21
23
  export declare function handleEvaluate(input: EvaluateInput, apiKey?: string): Promise<unknown>;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { callEndpoint, SpmApiError } from '../client/spm-api.js';
12
12
  export async function handleEvaluate(input, apiKey) {
13
- const { document, nano_app_id, main_expectation, all_sub_expectations, prior_clarifications = [], } = input;
13
+ const { document, nano_app_id, main_expectation, all_sub_expectations, prior_clarifications = [], user_context = '', about_company = '', } = input;
14
14
  if (!document?.trim()) {
15
15
  throw new SpmApiError('Document text is required', 'INVALID_INPUT');
16
16
  }
@@ -51,6 +51,8 @@ export async function handleEvaluate(input, apiKey) {
51
51
  all_sub_expectations: parsedSubExps,
52
52
  requirement_document: document,
53
53
  prior_clarifications: formattedClarifications,
54
+ user_context,
55
+ about_company,
54
56
  nano_app_id,
55
57
  };
56
58
  const result = await callEndpoint('extensionGapEvaluatorV3', payload, apiKey);
package/package.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "spm-mcp",
3
- "version": "0.1.0",
4
- "description": "Super Product Manager MCP Server AI-powered product document analysis",
3
+ "version": "0.3.0",
4
+ "description": "Super Product Manager MCP Server - AI-powered product document analysis for PRDs, roadmaps, and 30 PM document types",
5
+ "author": "Super Product Manager <chiranjeevi.gunturi@superproductmanager.ai>",
6
+ "homepage": "https://superproductmanager.ai",
5
7
  "type": "module",
6
8
  "main": "dist/index.js",
7
9
  "bin": {
@@ -23,6 +25,14 @@
23
25
  "@types/node": "^22.10.0",
24
26
  "typescript": "~5.7.0"
25
27
  },
28
+ "keywords": [
29
+ "mcp",
30
+ "mcp-server",
31
+ "model-context-protocol",
32
+ "product-management",
33
+ "document-review"
34
+ ],
35
+ "license": "MIT",
26
36
  "engines": {
27
37
  "node": ">=20"
28
38
  }
package/spm-mcp-0.1.0.tgz CHANGED
Binary file
@@ -5,6 +5,10 @@
5
5
 
6
6
  import { config } from '../config.js';
7
7
 
8
+ // Channel identifier — overridden by hosted/cloud-run.ts
9
+ let _channel = 'mcp-npm';
10
+ export function setChannel(ch: string) { _channel = ch; }
11
+
8
12
  export async function callJsonEndpoint(
9
13
  endpoint: string,
10
14
  body: Record<string, unknown> = {},
@@ -13,6 +17,8 @@ export async function callJsonEndpoint(
13
17
  const url = `${config.cloudFunctionsBase}/${endpoint}`;
14
18
  const headers: Record<string, string> = {
15
19
  'Content-Type': 'application/json',
20
+ 'Origin': 'https://mcp.superproductmanager.ai',
21
+ 'X-SPM-Channel': _channel,
16
22
  };
17
23
  if (apiKey) {
18
24
  headers['X-SPM-API-Key'] = apiKey;
@@ -19,11 +19,17 @@ const ENDPOINTS = {
19
19
 
20
20
  type EndpointName = keyof typeof ENDPOINTS;
21
21
 
22
+ // Channel identifier — shared with cloud-functions.ts
23
+ let _channel = 'mcp-npm';
24
+ export function setChannel(ch: string) { _channel = ch; }
25
+
22
26
  function getHeaders(apiKey?: string): Record<string, string> {
23
27
  const key = apiKey || config.apiKey;
24
28
  const headers: Record<string, string> = {
25
29
  'Content-Type': 'application/json',
26
30
  'Accept': 'text/event-stream, application/json',
31
+ 'Origin': 'https://mcp.superproductmanager.ai',
32
+ 'X-SPM-Channel': _channel,
27
33
  };
28
34
  if (key) {
29
35
  headers['X-SPM-API-Key'] = key;
package/src/config.ts CHANGED
@@ -1,26 +1,55 @@
1
1
  /**
2
2
  * SPM MCP Server configuration.
3
- * All credentials must be provided via environment variables.
3
+ *
4
+ * Only SPM_API_KEY is required from the user.
5
+ * Supabase values are public (protected by RLS) and default to production.
6
+ * API key is the security boundary: user-specific, hashed, revocable.
4
7
  */
5
8
 
6
- function requireEnv(name: string): string {
7
- const v = process.env[name];
8
- if (!v) throw new Error(`${name} environment variable is required`);
9
- return v;
9
+ import { readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ // ─── Public defaults (same values shipped in client-side bundle) ────
14
+
15
+ const DEFAULT_SUPABASE_URL = 'https://zrqngfkafphexqrteukm.supabase.co';
16
+ const DEFAULT_SUPABASE_ANON_KEY =
17
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpycW5nZmthZnBoZXhxcnRldWttIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTYyNjc2MTIsImV4cCI6MjAzMTg0MzYxMn0.YNeVJnNwoTRKPRUi3JT0JQXq8XGXZ1GI6q1sqL9l0b4';
18
+ const DEFAULT_CLOUD_FUNCTIONS_BASE = 'https://us-central1-litethink.cloudfunctions.net';
19
+
20
+ // ─── Config file persistence (~/.spm/config.json) ───────────────────
21
+
22
+ function loadConfigFile(): Record<string, string> {
23
+ try {
24
+ const configPath = join(homedir(), '.spm', 'config.json');
25
+ const raw = readFileSync(configPath, 'utf8');
26
+ return JSON.parse(raw);
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ const savedConfig = loadConfigFile();
33
+
34
+ // ─── Resolve API key: env var > config file ─────────────────────────
35
+
36
+ function resolveApiKey(): string {
37
+ const key = process.env.SPM_API_KEY || savedConfig.apiKey || '';
38
+ return key;
10
39
  }
11
40
 
12
41
  export const config = {
13
- /** Supabase project URL */
14
- supabaseUrl: requireEnv('SPM_SUPABASE_URL'),
42
+ /** Supabase project URL (public, RLS-protected) */
43
+ supabaseUrl: process.env.SPM_SUPABASE_URL || DEFAULT_SUPABASE_URL,
15
44
 
16
- /** Supabase anon key */
17
- supabaseAnonKey: requireEnv('SPM_SUPABASE_ANON_KEY'),
45
+ /** Supabase anon key (public, RLS-protected) */
46
+ supabaseAnonKey: process.env.SPM_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY,
18
47
 
19
48
  /** Base URL for Cloud Functions */
20
- cloudFunctionsBase: process.env.SPM_CLOUD_FUNCTIONS_BASE || 'https://us-central1-litethink.cloudfunctions.net',
49
+ cloudFunctionsBase: process.env.SPM_CLOUD_FUNCTIONS_BASE || DEFAULT_CLOUD_FUNCTIONS_BASE,
21
50
 
22
51
  /** User's SPM API key for authenticating with Cloud Functions */
23
- apiKey: process.env.SPM_API_KEY || '',
52
+ apiKey: resolveApiKey(),
24
53
 
25
54
  /** Port for hosted HTTP transport */
26
55
  port: parseInt(process.env.PORT || '8080', 10),
package/src/index.ts CHANGED
@@ -96,6 +96,12 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
96
96
  })).optional().describe(
97
97
  'Array of previous Q&A pairs from this clarification session. Empty for first question.',
98
98
  ),
99
+ user_context: z.string().optional().describe(
100
+ 'Context about the PM: role, experience level, company stage, research done, decisions made. Helps generate tailored questions.',
101
+ ),
102
+ about_company: z.string().optional().describe(
103
+ 'Context about the company/product: stage, team size, market, existing users, business model.',
104
+ ),
99
105
  },
100
106
  async (input) => {
101
107
  try {
@@ -129,6 +135,12 @@ export function createSpmMcpServer(options?: { apiKey?: string }): McpServer {
129
135
  })).describe(
130
136
  'All Q&A pairs accumulated from spm_clarify rounds. The evaluator uses these as evidence.',
131
137
  ),
138
+ user_context: z.string().optional().describe(
139
+ 'Context about the PM: role, experience level, company stage, research done, decisions made.',
140
+ ),
141
+ about_company: z.string().optional().describe(
142
+ 'Context about the company/product: stage, team size, market, existing users, business model.',
143
+ ),
132
144
  },
133
145
  async (input) => {
134
146
  try {
package/src/stdio.ts CHANGED
@@ -2,24 +2,118 @@
2
2
  /**
3
3
  * Stdio transport entry point for the SPM MCP Server.
4
4
  *
5
- * Used by Claude Code and other local MCP clients that spawn the server
6
- * as a subprocess. API key is read from SPM_API_KEY env var.
5
+ * Two modes:
6
+ * 1. Setup (interactive): `npx spm-mcp --setup` or auto-detected on first run
7
+ * Opens browser for login, prompts for API key, saves to ~/.spm/config.json
7
8
  *
8
- * Configure in .claude/settings.json:
9
- * {
10
- * "mcpServers": {
11
- * "spm": {
12
- * "command": "node",
13
- * "args": ["/path/to/packages/mcp-server/dist/src/stdio.js"],
14
- * "env": { "SPM_API_KEY": "spm_k_..." }
15
- * }
16
- * }
17
- * }
9
+ * 2. Server (non-interactive): started by Claude Code / Cursor as MCP subprocess
10
+ * Reads API key from ~/.spm/config.json or SPM_API_KEY env var
18
11
  */
19
12
 
20
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
14
  import { createSpmMcpServer } from './index.js';
15
+ import { config } from './config.js';
16
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
17
+ import { join } from 'path';
18
+ import { homedir } from 'os';
19
+ import { createInterface } from 'readline';
22
20
 
23
- const server = createSpmMcpServer();
24
- const transport = new StdioServerTransport();
25
- await server.connect(transport);
21
+ const CONFIG_DIR = join(homedir(), '.spm');
22
+ const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
23
+ const SETUP_URL = 'https://superproductmanager.web.app/#/profile';
24
+
25
+ function isInteractive(): boolean {
26
+ return process.stdin.isTTY === true;
27
+ }
28
+
29
+ function hasApiKey(): boolean {
30
+ return config.apiKey !== '' && config.apiKey.startsWith('spm_k_');
31
+ }
32
+
33
+ async function prompt(question: string): Promise<string> {
34
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
35
+ return new Promise((resolve) => {
36
+ rl.question(question, (answer) => {
37
+ rl.close();
38
+ resolve(answer.trim());
39
+ });
40
+ });
41
+ }
42
+
43
+ function saveApiKey(key: string): void {
44
+ let existing: Record<string, string> = {};
45
+ try {
46
+ existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
47
+ } catch { /* no existing config */ }
48
+
49
+ if (!existsSync(CONFIG_DIR)) {
50
+ mkdirSync(CONFIG_DIR, { recursive: true });
51
+ }
52
+ writeFileSync(CONFIG_PATH, JSON.stringify({ ...existing, apiKey: key }, null, 2) + '\n');
53
+ }
54
+
55
+ async function runSetup(): Promise<boolean> {
56
+ const log = (msg: string) => process.stderr.write(msg + '\n');
57
+
58
+ log('');
59
+ log(' Super Product Manager (SPM) MCP Server');
60
+ log(' ───────────────────────────────────────');
61
+ log('');
62
+ log(' No API key found. Let\'s set you up.');
63
+ log('');
64
+ log(` 1. Sign in at: ${SETUP_URL}`);
65
+ log(' 2. Go to Profile > Generate API Key');
66
+ log(' 3. Copy the key (starts with spm_k_)');
67
+ log('');
68
+
69
+ // Try to open browser
70
+ try {
71
+ const { exec } = await import('child_process');
72
+ const cmd = process.platform === 'darwin' ? 'open' :
73
+ process.platform === 'win32' ? 'start' : 'xdg-open';
74
+ exec(`${cmd} "${SETUP_URL}"`);
75
+ log(' Browser opened. Sign in and generate your key.');
76
+ } catch {
77
+ log(` Open this URL in your browser: ${SETUP_URL}`);
78
+ }
79
+
80
+ log('');
81
+ const key = await prompt(' Paste your API key here: ');
82
+
83
+ if (!key.startsWith('spm_k_')) {
84
+ log('');
85
+ log(' Invalid key. API keys start with spm_k_');
86
+ log(' Run `npx spm-mcp --setup` to try again.');
87
+ return false;
88
+ }
89
+
90
+ saveApiKey(key);
91
+ log('');
92
+ log(` Key saved to ${CONFIG_PATH}`);
93
+ log('');
94
+ log(' You\'re all set! Add SPM to your AI tool:');
95
+ log('');
96
+ log(' Claude Code: claude mcp add spm -- npx spm-mcp');
97
+ log(' Cursor: Add to .cursor/mcp.json');
98
+ log('');
99
+ return true;
100
+ }
101
+
102
+ // ─── Main ───────────────────────────────────────────────────────────
103
+
104
+ const wantsSetup = process.argv.includes('--setup');
105
+
106
+ if (wantsSetup || (isInteractive() && !hasApiKey())) {
107
+ // Interactive setup mode
108
+ const ok = await runSetup();
109
+ process.exit(ok ? 0 : 1);
110
+ } else if (!hasApiKey() && isInteractive()) {
111
+ // No key and interactive — guide the user
112
+ process.stderr.write('\n No SPM_API_KEY found. Run: npx spm-mcp --setup\n\n');
113
+ process.exit(1);
114
+ } else {
115
+ // MCP server mode (non-interactive, started by AI tool)
116
+ const server = createSpmMcpServer({ apiKey: config.apiKey });
117
+ const transport = new StdioServerTransport();
118
+ await server.connect(transport);
119
+ }
@@ -58,6 +58,19 @@ export const clarifyDefinition = {
58
58
  'Array of previous Q&A pairs from this clarification session. ' +
59
59
  'Pass as [{question, answer}, ...]. Empty array for first question.',
60
60
  },
61
+ user_context: {
62
+ type: 'string',
63
+ description:
64
+ 'Context about the PM: their role, experience level, company stage, ' +
65
+ 'what research they have done, what decisions they have already made. ' +
66
+ 'This helps generate questions tailored to the specific user rather than generic ones.',
67
+ },
68
+ about_company: {
69
+ type: 'string',
70
+ description:
71
+ 'Context about the company/product: stage, team size, market, ' +
72
+ 'existing users, business model. Helps ground questions in reality.',
73
+ },
61
74
  },
62
75
  required: ['document', 'nano_app_id', 'main_expectation', 'all_sub_expectations', 'target_sub_expectation'],
63
76
  },
@@ -70,6 +83,8 @@ export interface ClarifyInput {
70
83
  all_sub_expectations: string;
71
84
  target_sub_expectation: string;
72
85
  previous_answers?: Array<{ question: string; answer: string }>;
86
+ user_context?: string;
87
+ about_company?: string;
73
88
  }
74
89
 
75
90
  export async function handleClarify(input: ClarifyInput, apiKey?: string): Promise<unknown> {
@@ -80,6 +95,8 @@ export async function handleClarify(input: ClarifyInput, apiKey?: string): Promi
80
95
  all_sub_expectations,
81
96
  target_sub_expectation,
82
97
  previous_answers = [],
98
+ user_context = '',
99
+ about_company = '',
83
100
  } = input;
84
101
 
85
102
  if (!document?.trim()) {
@@ -117,6 +134,8 @@ export async function handleClarify(input: ClarifyInput, apiKey?: string): Promi
117
134
  recent_recommendation: '',
118
135
  latest_review: '',
119
136
  gap_coverage: '',
137
+ user_context,
138
+ about_company,
120
139
  nano_app_id,
121
140
  };
122
141
 
@@ -17,6 +17,8 @@ export interface EvaluateInput {
17
17
  main_expectation: string;
18
18
  all_sub_expectations: string;
19
19
  prior_clarifications: Array<{ question: string; answer: string }>;
20
+ user_context?: string;
21
+ about_company?: string;
20
22
  }
21
23
 
22
24
  export async function handleEvaluate(input: EvaluateInput, apiKey?: string): Promise<unknown> {
@@ -26,6 +28,8 @@ export async function handleEvaluate(input: EvaluateInput, apiKey?: string): Pro
26
28
  main_expectation,
27
29
  all_sub_expectations,
28
30
  prior_clarifications = [],
31
+ user_context = '',
32
+ about_company = '',
29
33
  } = input;
30
34
 
31
35
  if (!document?.trim()) {
@@ -71,6 +75,8 @@ export async function handleEvaluate(input: EvaluateInput, apiKey?: string): Pro
71
75
  all_sub_expectations: parsedSubExps,
72
76
  requirement_document: document,
73
77
  prior_clarifications: formattedClarifications,
78
+ user_context,
79
+ about_company,
74
80
  nano_app_id,
75
81
  };
76
82