second-opinion-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +323 -0
  2. package/dist/config.d.ts +31 -0
  3. package/dist/config.js +84 -0
  4. package/dist/context/bundler.d.ts +51 -0
  5. package/dist/context/bundler.js +481 -0
  6. package/dist/context/bundler.test.d.ts +1 -0
  7. package/dist/context/bundler.test.js +275 -0
  8. package/dist/context/git.d.ts +21 -0
  9. package/dist/context/git.js +102 -0
  10. package/dist/context/imports.d.ts +43 -0
  11. package/dist/context/imports.js +197 -0
  12. package/dist/context/imports.test.d.ts +1 -0
  13. package/dist/context/imports.test.js +147 -0
  14. package/dist/context/index.d.ts +6 -0
  15. package/dist/context/index.js +6 -0
  16. package/dist/context/session.d.ts +35 -0
  17. package/dist/context/session.js +317 -0
  18. package/dist/context/tests.d.ts +13 -0
  19. package/dist/context/tests.js +83 -0
  20. package/dist/context/types.d.ts +16 -0
  21. package/dist/context/types.js +100 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +6 -0
  24. package/dist/output/writer.d.ts +34 -0
  25. package/dist/output/writer.js +162 -0
  26. package/dist/output/writer.test.d.ts +1 -0
  27. package/dist/output/writer.test.js +175 -0
  28. package/dist/providers/base.d.ts +24 -0
  29. package/dist/providers/base.js +77 -0
  30. package/dist/providers/base.test.d.ts +1 -0
  31. package/dist/providers/base.test.js +91 -0
  32. package/dist/providers/gemini.d.ts +8 -0
  33. package/dist/providers/gemini.js +43 -0
  34. package/dist/providers/index.d.ts +8 -0
  35. package/dist/providers/index.js +31 -0
  36. package/dist/providers/openai.d.ts +8 -0
  37. package/dist/providers/openai.js +39 -0
  38. package/dist/server.d.ts +3 -0
  39. package/dist/server.js +181 -0
  40. package/dist/test-utils.d.ts +71 -0
  41. package/dist/test-utils.js +136 -0
  42. package/dist/tools/review.d.ts +76 -0
  43. package/dist/tools/review.js +199 -0
  44. package/dist/utils/index.d.ts +1 -0
  45. package/dist/utils/index.js +1 -0
  46. package/dist/utils/tokens.d.ts +26 -0
  47. package/dist/utils/tokens.js +27 -0
  48. package/package.json +61 -0
  49. package/scripts/install-config.js +51 -0
  50. package/second-opinion.skill.md +34 -0
  51. package/templates/second-opinion.md +54 -0
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # Second Opinion
2
+
3
+ Get code reviews and feedback from Gemini or GPT while working in Claude Code.
4
+
5
+ Second Opinion is an MCP server that automatically collects context from your Claude Code session—files you've read, edited, and their dependencies—and sends it to another LLM for review. No copy-pasting, no context switching.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Add to Claude Code (one command)
11
+ claude mcp add second-opinion \
12
+ -e GEMINI_API_KEY="$(cat ~/.secrets/gemini-key)" \
13
+ -- npx second-opinion-mcp
14
+ ```
15
+
16
+ Then in Claude Code:
17
+
18
+ ```
19
+ /second-opinion
20
+ ```
21
+
22
+ That's it. The review appears in `second-opinions/`.
23
+
24
+ ## Features
25
+
26
+ ### Automatic Context Collection
27
+
28
+ Second Opinion reads your Claude Code session to understand what you're working on:
29
+
30
+ - **Session files** — Files you read, edited, or created
31
+ - **Conversation** — What you asked Claude to do (code blocks stripped to avoid stale references)
32
+ - **Dependencies** — Files imported by your modified code
33
+ - **Dependents** — Files that import your modified code
34
+ - **Tests** — Test files related to your changes
35
+ - **Types** — TypeScript/JSDoc type definitions
36
+
37
+ ### Custom Tasks
38
+
39
+ Don't just get code reviews—ask for anything:
40
+
41
+ ```
42
+ /second-opinion Evaluate the error handling strategy across this codebase
43
+
44
+ /second-opinion Write user documentation for the API changes
45
+
46
+ /second-opinion openai Identify potential performance bottlenecks
47
+ ```
48
+
49
+ ### Multiple Providers
50
+
51
+ Switch between Gemini and GPT:
52
+
53
+ ```
54
+ /second-opinion gemini Review this code # Uses Gemini (default)
55
+ /second-opinion openai Review this code # Uses GPT
56
+ ```
57
+
58
+ ### Smart Token Budgeting
59
+
60
+ Context is prioritized to fit within token limits:
61
+
62
+ 1. Explicitly included files (highest priority)
63
+ 2. Session files (what you worked on)
64
+ 3. Git changes
65
+ 4. Dependencies
66
+ 5. Dependents
67
+ 6. Tests
68
+ 7. Type definitions
69
+
70
+ Files that don't fit are listed in the output so you know what was omitted.
71
+
72
+ ### Include Additional Files
73
+
74
+ Reference files outside your session:
75
+
76
+ ```
77
+ /second-opinion The previous review at reviews/initial.md has been addressed. Verify the fixes.
78
+ ```
79
+
80
+ ## Examples
81
+
82
+ ### Basic Code Review
83
+
84
+ ```
85
+ > /second-opinion
86
+
87
+ Review complete! Written to second-opinions/add-auth-flow.gemini.review.md
88
+ - Analyzed 14 files (52,000 tokens)
89
+ - Key findings: Missing input validation in login handler,
90
+ consider rate limiting for auth endpoints
91
+ ```
92
+
93
+ ### Security Audit
94
+
95
+ ```
96
+ > /second-opinion openai Audit this code for security vulnerabilities.
97
+ Focus on authentication, input validation, and data exposure.
98
+
99
+ Analysis complete! Written to second-opinions/add-auth-flow.openai.security-audit.md
100
+ ```
101
+
102
+ ### Architecture Review
103
+
104
+ ```
105
+ > /second-opinion Evaluate the architecture of this feature.
106
+ Is the separation of concerns appropriate? Are there any circular dependencies?
107
+ ```
108
+
109
+ ### Documentation Generation
110
+
111
+ ```
112
+ > /second-opinion Write API documentation for the changes made in this session.
113
+ Include request/response examples.
114
+ ```
115
+
116
+ ### Compare Perspectives
117
+
118
+ Get reviews from both providers:
119
+
120
+ ```
121
+ > /second-opinion gemini Review this implementation
122
+ > /second-opinion openai Review this implementation
123
+ ```
124
+
125
+ ## Security
126
+
127
+ Second Opinion implements multiple layers of protection:
128
+
129
+ ### What Data Is Sent
130
+
131
+ When you use Second Opinion, the following data may be sent to the external LLM (Gemini or OpenAI):
132
+
133
+ - **File contents**: Source code from your project and any explicitly included files
134
+ - **Conversation context**: A summary of your Claude Code session (what you asked, not your full chat history)
135
+ - **File metadata**: File paths relative to your project
136
+
137
+ The tool does NOT send:
138
+ - Your API keys
139
+ - System files or shell history
140
+ - Files blocked by sensitive path patterns
141
+
142
+ ### Sensitive Path Blocking
143
+
144
+ The following paths are always blocked, even when explicitly requested:
145
+
146
+ - SSH keys and config (`~/.ssh/`)
147
+ - AWS credentials (`~/.aws/`)
148
+ - GPG keys (`~/.gnupg/`)
149
+ - Cloud configs (`~/.config/gcloud/`, `~/.kube/`)
150
+ - Git internals (`/.git/`)
151
+ - Auth files (`.netrc`, `.npmrc`, `.pypirc`)
152
+ - Private keys (`*.pem`, `*.key`, `id_rsa`, `id_ed25519`)
153
+ - Service account credentials
154
+ - Environment files (`.env`, `.env.local`, `.env.production`)
155
+ - Terraform secrets (`.tfvars`, `terraform.tfstate`)
156
+ - Kubernetes secrets (`secret.yaml`, `secret.yml`)
157
+ - Shell history (`.bash_history`, `.zsh_history`)
158
+
159
+ ### External File Protection
160
+
161
+ By default, files outside your project directory are blocked. If you need to include external files, you must explicitly set `allowExternalFiles: true`. This prevents accidental exfiltration of files from other projects or system locations.
162
+
163
+ ### Symlink Protection
164
+
165
+ All paths are resolved via `realpathSync()` before reading. A symlink pointing to `~/.ssh/id_rsa` will be blocked even if it lives inside your project.
166
+
167
+ ### Output Directory Validation
168
+
169
+ Reviews are only written within your project directory. Path traversal attempts (e.g., `../../../etc/passwd`) are rejected.
170
+
171
+ ### Egress Audit Trail
172
+
173
+ Every review creates a companion `.egress.json` file that records:
174
+ - Exactly which files were sent to the external LLM
175
+ - Which files were blocked and why
176
+ - Timestamp and provider information
177
+
178
+ This allows you to audit what data left your system.
179
+
180
+ ### API Key Safety
181
+
182
+ Never paste API keys directly in the terminal—they get saved to shell history. Instead:
183
+
184
+ ```bash
185
+ # Read from a file
186
+ export GEMINI_API_KEY=$(cat ~/.secrets/gemini-key)
187
+
188
+ # Use a password manager
189
+ export OPENAI_API_KEY=$(op read "op://Private/OpenAI/api-key")
190
+
191
+ # Set via MCP config
192
+ claude mcp add second-opinion \
193
+ -e GEMINI_API_KEY="$(cat ~/.secrets/gemini-key)" \
194
+ -- npx second-opinion-mcp
195
+ ```
196
+
197
+ **Never paste API keys directly in Claude Code chat.** Keys in chat messages could be logged or sent to external providers.
198
+
199
+ ## Configuration
200
+
201
+ ### Environment Variables
202
+
203
+ | Variable | Default | Description |
204
+ |----------|---------|-------------|
205
+ | `GEMINI_API_KEY` | — | API key for Google Gemini |
206
+ | `OPENAI_API_KEY` | — | API key for OpenAI |
207
+ | `GEMINI_MODEL` | `gemini-2.0-flash-exp` | Gemini model to use |
208
+ | `OPENAI_MODEL` | `gpt-4o` | OpenAI model to use |
209
+ | `DEFAULT_PROVIDER` | `gemini` | Default provider when not specified |
210
+ | `MAX_CONTEXT_TOKENS` | `100000` | Maximum tokens for context |
211
+ | `REVIEWS_DIR` | `second-opinions` | Output directory (relative to project) |
212
+
213
+ ### Config File
214
+
215
+ Create `~/.config/second-opinion/config.json`:
216
+
217
+ ```json
218
+ {
219
+ "geminiApiKey": "your-key",
220
+ "openaiApiKey": "your-key",
221
+ "defaultProvider": "gemini",
222
+ "geminiModel": "gemini-2.0-flash-exp",
223
+ "openaiModel": "gpt-4o",
224
+ "maxContextTokens": 100000,
225
+ "reviewsDir": "second-opinions"
226
+ }
227
+ ```
228
+
229
+ Environment variables take precedence over the config file.
230
+
231
+ ### Custom Review Instructions
232
+
233
+ Create `~/.config/second-opinion/second-opinion.md` for global instructions, or `second-opinion.md` in your project root for project-specific instructions:
234
+
235
+ ```markdown
236
+ # Review Instructions
237
+
238
+ Focus on:
239
+ - Security vulnerabilities (OWASP Top 10)
240
+ - Performance implications
241
+ - Error handling completeness
242
+ - Test coverage gaps
243
+
244
+ Our stack: TypeScript, React, PostgreSQL
245
+ Coding standards: Airbnb style guide
246
+ ```
247
+
248
+ ## Tool Parameters
249
+
250
+ When calling the MCP tool directly:
251
+
252
+ | Parameter | Required | Default | Description |
253
+ |-----------|----------|---------|-------------|
254
+ | `provider` | Yes | — | `"gemini"` or `"openai"` |
255
+ | `projectPath` | Yes | — | Absolute path to project |
256
+ | `task` | No | — | Custom prompt (defaults to code review) |
257
+ | `sessionId` | No | latest | Claude Code session ID |
258
+ | `sessionName` | No | auto | Name for output file |
259
+ | `includeFiles` | No | — | Additional files/folders to include |
260
+ | `allowExternalFiles` | No | `false` | Allow files outside project (required for external paths in includeFiles) |
261
+ | `dryRun` | No | `false` | Preview what would be sent without calling external API |
262
+ | `includeConversation` | No | `true` | Include conversation context |
263
+ | `includeDependencies` | No | `true` | Include imported files |
264
+ | `includeDependents` | No | `true` | Include importing files |
265
+ | `includeTests` | No | `true` | Include test files |
266
+ | `includeTypes` | No | `true` | Include type definitions |
267
+ | `maxTokens` | No | `100000` | Context token budget |
268
+ | `focusAreas` | No | — | Specific areas to focus on |
269
+
270
+ ## How It Works
271
+
272
+ ```
273
+ ┌─────────────────────────────────────────────────────────────────┐
274
+ │ Claude Code │
275
+ │ │
276
+ │ You: "Add user authentication" │
277
+ │ Claude: [reads files, writes code, runs tests] │
278
+ │ You: "/second-opinion" │
279
+ │ │
280
+ └─────────────────────┬───────────────────────────────────────────┘
281
+
282
+
283
+ ┌─────────────────────────────────────────────────────────────────┐
284
+ │ Second Opinion MCP │
285
+ │ │
286
+ │ 1. Parse Claude Code session logs │
287
+ │ 2. Collect files read/written + their content │
288
+ │ 3. Resolve dependencies and dependents │
289
+ │ 4. Find related tests and types │
290
+ │ 5. Bundle within token budget │
291
+ │ 6. Send to Gemini/GPT │
292
+ │ 7. Write response to second-opinions/ │
293
+ │ │
294
+ └─────────────────────┬───────────────────────────────────────────┘
295
+
296
+
297
+ ┌─────────────────────────────────────────────────────────────────┐
298
+ │ second-opinions/add-auth.gemini.review.md │
299
+ │ │
300
+ │ # Code Review - add-auth │
301
+ │ **Provider:** gemini │
302
+ │ │
303
+ │ ## Summary │
304
+ │ The authentication implementation is solid... │
305
+ │ │
306
+ │ ## Critical Issues │
307
+ │ - Missing rate limiting on login endpoint │
308
+ │ │
309
+ │ ## Suggestions │
310
+ │ - Consider adding refresh token rotation │
311
+ │ │
312
+ └─────────────────────────────────────────────────────────────────┘
313
+ ```
314
+
315
+ ## Requirements
316
+
317
+ - Node.js 18+
318
+ - Claude Code CLI
319
+ - At least one API key (Gemini or OpenAI)
320
+
321
+ ## License
322
+
323
+ MIT
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ export declare const ConfigSchema: z.ZodObject<{
3
+ geminiApiKey: z.ZodOptional<z.ZodString>;
4
+ openaiApiKey: z.ZodOptional<z.ZodString>;
5
+ defaultProvider: z.ZodDefault<z.ZodEnum<["gemini", "openai"]>>;
6
+ geminiModel: z.ZodDefault<z.ZodString>;
7
+ openaiModel: z.ZodDefault<z.ZodString>;
8
+ maxContextTokens: z.ZodDefault<z.ZodNumber>;
9
+ reviewsDir: z.ZodDefault<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ defaultProvider: "gemini" | "openai";
12
+ geminiModel: string;
13
+ openaiModel: string;
14
+ maxContextTokens: number;
15
+ reviewsDir: string;
16
+ geminiApiKey?: string | undefined;
17
+ openaiApiKey?: string | undefined;
18
+ }, {
19
+ geminiApiKey?: string | undefined;
20
+ openaiApiKey?: string | undefined;
21
+ defaultProvider?: "gemini" | "openai" | undefined;
22
+ geminiModel?: string | undefined;
23
+ openaiModel?: string | undefined;
24
+ maxContextTokens?: number | undefined;
25
+ reviewsDir?: string | undefined;
26
+ }>;
27
+ export type Config = z.infer<typeof ConfigSchema>;
28
+ export declare function getConfigDir(): string;
29
+ export declare function getClaudeProjectsDir(): string;
30
+ export declare function loadConfig(): Config;
31
+ export declare function loadReviewInstructions(projectPath?: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ export const ConfigSchema = z.object({
6
+ geminiApiKey: z.string().optional(),
7
+ openaiApiKey: z.string().optional(),
8
+ defaultProvider: z.enum(["gemini", "openai"]).default("gemini"),
9
+ geminiModel: z.string().default("gemini-3-flash-preview"),
10
+ openaiModel: z.string().default("gpt-5.2"),
11
+ maxContextTokens: z.number().default(100000),
12
+ reviewsDir: z.string().default("second-opinions"),
13
+ });
14
+ export function getConfigDir() {
15
+ return path.join(os.homedir(), ".config", "second-opinion");
16
+ }
17
+ export function getClaudeProjectsDir() {
18
+ return path.join(os.homedir(), ".claude", "projects");
19
+ }
20
+ export function loadConfig() {
21
+ const configDir = getConfigDir();
22
+ const configPath = path.join(configDir, "config.json");
23
+ let fileConfig = {};
24
+ if (fs.existsSync(configPath)) {
25
+ try {
26
+ fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
27
+ }
28
+ catch (error) {
29
+ console.error(`Warning: Invalid JSON in config file ${configPath}. Using defaults.`);
30
+ }
31
+ }
32
+ const config = ConfigSchema.parse({
33
+ geminiApiKey: process.env.GEMINI_API_KEY || fileConfig.geminiApiKey,
34
+ openaiApiKey: process.env.OPENAI_API_KEY || fileConfig.openaiApiKey,
35
+ defaultProvider: process.env.DEFAULT_PROVIDER || fileConfig.defaultProvider,
36
+ geminiModel: process.env.GEMINI_MODEL || fileConfig.geminiModel,
37
+ openaiModel: process.env.OPENAI_MODEL || fileConfig.openaiModel,
38
+ maxContextTokens: process.env.MAX_CONTEXT_TOKENS
39
+ ? parseInt(process.env.MAX_CONTEXT_TOKENS)
40
+ : fileConfig.maxContextTokens,
41
+ reviewsDir: process.env.REVIEWS_DIR || fileConfig.reviewsDir,
42
+ });
43
+ return config;
44
+ }
45
+ export function loadReviewInstructions(projectPath) {
46
+ const configDir = getConfigDir();
47
+ // Check project-local first
48
+ if (projectPath) {
49
+ const projectInstructions = path.join(projectPath, "second-opinion.md");
50
+ if (fs.existsSync(projectInstructions)) {
51
+ return fs.readFileSync(projectInstructions, "utf-8");
52
+ }
53
+ }
54
+ // Fall back to global
55
+ const globalInstructions = path.join(configDir, "second-opinion.md");
56
+ if (fs.existsSync(globalInstructions)) {
57
+ return fs.readFileSync(globalInstructions, "utf-8");
58
+ }
59
+ // Default instructions
60
+ return `# Code Review Instructions
61
+
62
+ You are a code reviewer providing a second opinion on code changes.
63
+
64
+ ## Your Role
65
+ - Review the code changes objectively
66
+ - Identify potential issues, bugs, or improvements
67
+ - Be constructive and specific in your feedback
68
+ - Consider security, performance, and maintainability
69
+
70
+ ## Review Focus
71
+ - Security vulnerabilities and best practices
72
+ - Performance considerations
73
+ - Code clarity and maintainability
74
+ - Error handling and edge cases
75
+ - Testing coverage
76
+
77
+ ## Output Format
78
+ Structure your review with:
79
+ 1. **Summary** (2-3 sentences overview)
80
+ 2. **Critical Issues** (if any - things that must be fixed)
81
+ 3. **Suggestions** (improvements that would be nice)
82
+ 4. **What's Done Well** (positive feedback)
83
+ `;
84
+ }
@@ -0,0 +1,51 @@
1
+ export interface BlockedFile {
2
+ path: string;
3
+ reason: "sensitive_path" | "outside_project_requires_allowExternalFiles";
4
+ }
5
+ export interface BundleOptions {
6
+ projectPath: string;
7
+ sessionId?: string;
8
+ includeConversation?: boolean;
9
+ includeDependencies?: boolean;
10
+ includeDependents?: boolean;
11
+ includeTests?: boolean;
12
+ includeTypes?: boolean;
13
+ includeFiles?: string[];
14
+ allowExternalFiles?: boolean;
15
+ maxTokens?: number;
16
+ }
17
+ export interface FileEntry {
18
+ path: string;
19
+ content: string;
20
+ category: "session" | "git" | "dependency" | "dependent" | "test" | "type" | "explicit";
21
+ tokenEstimate: number;
22
+ }
23
+ export interface OmittedFile {
24
+ path: string;
25
+ category: FileEntry["category"];
26
+ tokenEstimate: number;
27
+ reason: "budget_exceeded" | "outside_project" | "sensitive_path" | "outside_project_requires_allowExternalFiles";
28
+ }
29
+ export interface ContextBundle {
30
+ conversationContext: string;
31
+ files: FileEntry[];
32
+ omittedFiles: OmittedFile[];
33
+ totalTokens: number;
34
+ categories: {
35
+ session: number;
36
+ git: number;
37
+ dependency: number;
38
+ dependent: number;
39
+ test: number;
40
+ type: number;
41
+ explicit: number;
42
+ };
43
+ }
44
+ /**
45
+ * Collect and bundle all context for review
46
+ */
47
+ export declare function bundleContext(options: BundleOptions): Promise<ContextBundle>;
48
+ /**
49
+ * Format the bundle as markdown for the reviewer
50
+ */
51
+ export declare function formatBundleAsMarkdown(bundle: ContextBundle, projectPath: string): string;