@j0hanz/prompt-tuner-mcp-server 1.0.9 → 1.0.10

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 (79) hide show
  1. package/CONFIGURATION.md +15 -9
  2. package/README.md +28 -29
  3. package/dist/config/constants.js +0 -1
  4. package/dist/config/env.js +0 -5
  5. package/dist/config/patterns.d.ts +0 -7
  6. package/dist/config/patterns.js +0 -7
  7. package/dist/config/types.d.ts +11 -21
  8. package/dist/index.js +33 -72
  9. package/dist/lib/errors.js +0 -5
  10. package/dist/lib/llm-json.js +37 -39
  11. package/dist/lib/llm-providers.js +0 -3
  12. package/dist/lib/llm-runtime.js +22 -5
  13. package/dist/lib/output-normalization.js +12 -32
  14. package/dist/lib/output-validation.js +21 -55
  15. package/dist/lib/prompt-analysis/format.js +0 -4
  16. package/dist/lib/tool-helpers.d.ts +5 -5
  17. package/dist/lib/tool-helpers.js +29 -44
  18. package/dist/lib/tool-resources.js +2 -6
  19. package/dist/lib/validation.d.ts +0 -4
  20. package/dist/lib/validation.js +0 -36
  21. package/dist/schemas/index.js +0 -1
  22. package/dist/schemas/inputs.d.ts +4 -4
  23. package/dist/schemas/llm-responses.d.ts +12 -12
  24. package/dist/schemas/llm-responses.js +0 -7
  25. package/dist/schemas/outputs.d.ts +40 -40
  26. package/dist/server.d.ts +1 -1
  27. package/dist/server.js +13 -10
  28. package/dist/tools/analyze-prompt.js +20 -23
  29. package/dist/tools/optimize-prompt/validation.js +23 -16
  30. package/dist/tools/optimize-prompt.js +222 -16
  31. package/dist/tools/refine-prompt.js +88 -41
  32. package/dist/tools/validate-prompt/constants.d.ts +3 -0
  33. package/dist/tools/validate-prompt/constants.js +13 -0
  34. package/dist/tools/validate-prompt/prompt.d.ts +1 -1
  35. package/dist/tools/validate-prompt/prompt.js +2 -1
  36. package/dist/tools/validate-prompt.js +105 -37
  37. package/package.json +5 -5
  38. package/src/config/constants.ts +0 -2
  39. package/src/config/env.ts +0 -5
  40. package/src/config/patterns.ts +0 -28
  41. package/src/config/types.ts +15 -33
  42. package/src/index.ts +35 -80
  43. package/src/lib/errors.ts +0 -5
  44. package/src/lib/llm-json.ts +49 -60
  45. package/src/lib/llm-providers.ts +0 -3
  46. package/src/lib/llm-runtime.ts +33 -10
  47. package/src/lib/output-normalization.ts +16 -34
  48. package/src/lib/output-validation.ts +21 -68
  49. package/src/lib/prompt-analysis/format.ts +0 -4
  50. package/src/lib/tool-helpers.ts +67 -114
  51. package/src/lib/tool-resources.ts +2 -8
  52. package/src/lib/validation.ts +0 -76
  53. package/src/schemas/index.ts +0 -2
  54. package/src/schemas/llm-responses.ts +0 -7
  55. package/src/server.ts +14 -10
  56. package/src/tools/analyze-prompt.ts +41 -39
  57. package/src/tools/optimize-prompt.ts +387 -28
  58. package/src/tools/refine-prompt.ts +186 -113
  59. package/src/tools/validate-prompt.ts +146 -62
  60. package/tests/integration.test.ts +0 -2
  61. package/benchmark/llm-json.bench.ts +0 -96
  62. package/benchmark/output-validation.bench.ts +0 -108
  63. package/benchmark/prompt-analysis.bench.ts +0 -105
  64. package/src/lib/prompt-analysis.ts +0 -5
  65. package/src/tools/index.ts +0 -22
  66. package/src/tools/optimize-prompt/constants.ts +0 -66
  67. package/src/tools/optimize-prompt/formatters.ts +0 -100
  68. package/src/tools/optimize-prompt/inputs.ts +0 -40
  69. package/src/tools/optimize-prompt/output.ts +0 -96
  70. package/src/tools/optimize-prompt/prompt.ts +0 -19
  71. package/src/tools/optimize-prompt/run.ts +0 -27
  72. package/src/tools/optimize-prompt/types.ts +0 -35
  73. package/src/tools/optimize-prompt/validation.ts +0 -114
  74. package/src/tools/refine-prompt/formatters.ts +0 -82
  75. package/src/tools/refine-prompt/types.ts +0 -22
  76. package/src/tools/validate-prompt/formatters.ts +0 -77
  77. package/src/tools/validate-prompt/prompt.ts +0 -40
  78. package/src/tools/validate-prompt/types.ts +0 -1
  79. package/src/types/regexp-escape.d.ts +0 -3
package/CONFIGURATION.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # PromptTuner MCP Configuration Guide
2
2
 
3
- PromptTuner MCP is configured entirely via environment variables. Set them in your MCP client configuration (for example `mcp.json`, `claude_desktop_config.json`) or a `.env` file.
3
+ PromptTuner MCP is configured entirely via environment variables. Set them in your MCP client configuration (for example `mcp.json`, `claude_desktop_config.json`) or a `.env` file. Node.js >= 22.0.0 is required (see `package.json` engines).
4
4
 
5
5
  ## Required configuration
6
6
 
@@ -33,6 +33,10 @@ Set `LLM_MODEL` to override the default model for the chosen provider.
33
33
  | `LLM_MAX_TOKENS` | `8000` | Upper bound for model output tokens. |
34
34
  | `LLM_TIMEOUT_MS` | `60000` | Per-request timeout (ms). |
35
35
 
36
+ All numeric values are parsed as integers. Invalid values or values below the minimum thresholds will fail startup validation.
37
+
38
+ Minimums: `MAX_PROMPT_LENGTH` >= 1, `LLM_MAX_TOKENS` >= 1, `LLM_TIMEOUT_MS` >= 1000.
39
+
36
40
  ### Prompt length enforcement
37
41
 
38
42
  - Input is trimmed before validation.
@@ -61,13 +65,15 @@ Tool max tokens are derived from `LLM_MAX_TOKENS`:
61
65
 
62
66
  Retries use exponential backoff with jitter and stop when the total timeout is exceeded.
63
67
 
68
+ Minimums: `RETRY_MAX_ATTEMPTS` >= 0, `RETRY_BASE_DELAY_MS` >= 100, `RETRY_MAX_DELAY_MS` >= 1000, `RETRY_TOTAL_TIMEOUT_MS` >= 10000.
69
+
64
70
  ## Logging and error context (optional)
65
71
 
66
- | Variable | Default | Description |
67
- | ----------------------- | ------- | -------------------------------------------------------------- |
68
- | `DEBUG` | `false` | Enables debug logging. Logs are written to stderr. |
69
- | `LOG_FORMAT` | `text` | Parsed but currently unused (logging output is JSON via pino). |
70
- | `INCLUDE_ERROR_CONTEXT` | `false` | Adds a sanitized prompt snippet (up to 200 chars) to errors. |
72
+ | Variable | Default | Description |
73
+ | ----------------------- | ------- | ---------------------------------------------------------------------------------------- |
74
+ | `DEBUG` | `false` | Enables debug logging (set to the string `true` or `false`). Logs are written to stderr. |
75
+ | `LOG_FORMAT` | `text` | Accepted values: `json`, `text`. Currently ignored; output is always JSON via pino. |
76
+ | `INCLUDE_ERROR_CONTEXT` | `false` | Adds a sanitized prompt snippet (up to 200 chars) to errors. |
71
77
 
72
78
  ## Provider-specific settings
73
79
 
@@ -179,7 +185,7 @@ The following behaviors are hardcoded for stability:
179
185
 
180
186
  If you have an old `.env` file, remove unused settings:
181
187
 
182
- - `PORT`, `HOST`, `CORS_ORIGIN` (stdio transport only; `--http` is reserved).
188
+ - `PORT`, `HOST`, `CORS_ORIGIN` (stdio transport only; no HTTP listener).
183
189
  - `API_KEY` (no server-level auth).
184
190
  - `LOG_LEVEL` (use `DEBUG=true` or false).
185
191
  - `RATE_LIMIT`, `RATE_WINDOW_MS` (no server-side rate limiting).
@@ -192,7 +198,7 @@ If you have an old `.env` file, remove unused settings:
192
198
 
193
199
  ### Prompt rejected
194
200
 
195
- - Reduce `MAX_PROMPT_LENGTH` or trim the input to remove excessive whitespace.
201
+ - Shorten the prompt, remove excessive whitespace, or increase `MAX_PROMPT_LENGTH`.
196
202
 
197
203
  ### Timeout errors
198
204
 
@@ -215,4 +221,4 @@ If you have an old `.env` file, remove unused settings:
215
221
  2. Use input variables for secrets (for example `"OPENAI_API_KEY": "${input:openai-api-key}"`).
216
222
  3. Start with defaults and tune only when needed.
217
223
  4. Enable `DEBUG=true` temporarily for troubleshooting.
218
- 5. Prefer JSON logging in production (current output is JSON via pino).
224
+ 5. Logs are JSON via pino; plan parsing/collection accordingly.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@j0hanz/prompt-tuner-mcp-server.svg)](https://www.npmjs.com/package/@j0hanz/prompt-tuner-mcp-server)
6
6
  [![License](https://img.shields.io/npm/l/@j0hanz/prompt-tuner-mcp-server)](LICENSE)
7
- [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org/)
7
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen)](https://nodejs.org/)
8
8
 
9
9
  PromptTuner MCP is an MCP server that refines, analyzes, optimizes, and validates prompts using OpenAI, Anthropic, or Google Gemini.
10
10
 
@@ -14,7 +14,7 @@ PromptTuner MCP is an MCP server that refines, analyzes, optimizes, and validate
14
14
  2. Resolves the target format (`auto` uses heuristics; falls back to `gpt` if no format is detected).
15
15
  3. Calls the selected provider with retry and timeout controls.
16
16
  4. Validates and normalizes LLM output, falling back to stricter prompts or the `basic` technique when needed.
17
- 5. Returns human-readable text plus machine-friendly `structuredContent` (and prompt files for refined/optimized outputs).
17
+ 5. Returns human-readable text plus machine-friendly `structuredContent` (and resource blocks for refined/optimized outputs).
18
18
 
19
19
  ## Features
20
20
 
@@ -27,7 +27,7 @@ PromptTuner MCP is an MCP server that refines, analyzes, optimizes, and validate
27
27
 
28
28
  ## Quick Start
29
29
 
30
- PromptTuner runs over stdio (MCP default). The `--http` flag shown in some scripts is reserved and currently has no effect.
30
+ PromptTuner runs over stdio only. The `dev:http` and `start:http` scripts are compatibility aliases (no HTTP transport yet).
31
31
 
32
32
  ### Claude Desktop
33
33
 
@@ -95,11 +95,11 @@ Returns: `ok`, `hasTypos`, `isVague`, `missingContext`, `suggestions`, `score`,
95
95
 
96
96
  Apply multiple techniques sequentially and return before/after scores.
97
97
 
98
- | Parameter | Type | Required | Default | Notes |
99
- | -------------- | -------- | -------- | ----------- | ----------------------------------- |
100
- | `prompt` | string | Yes | - | Trimmed and length-checked. |
101
- | `techniques` | string[] | No | `["basic"]` | 1-6 techniques; duplicates removed. |
102
- | `targetFormat` | string | No | `auto` | `auto`, `claude`, `gpt`, `json`. |
98
+ | Parameter | Type | Required | Default | Notes |
99
+ | -------------- | -------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
100
+ | `prompt` | string | Yes | - | Trimmed and length-checked. |
101
+ | `techniques` | string[] | No | `["basic"]` | 1-6 techniques; duplicates removed. `comprehensive` expands to `basic -> roleBased -> structured -> fewShot -> chainOfThought`. |
102
+ | `targetFormat` | string | No | `auto` | `auto`, `claude`, `gpt`, `json`. |
103
103
 
104
104
  Returns: `ok`, `original`, `optimized`, `techniquesApplied`, `targetFormat`, `beforeScore`, `afterScore`, `scoreDelta`, `improvements`, `usedFallback`, `scoreAdjusted`, `overallSource`, `provider`, `model`.
105
105
 
@@ -119,10 +119,10 @@ Token limits used for `validate_prompt`: `claude` 200000, `gpt` 128000, `gemini`
119
119
 
120
120
  ## Response Format
121
121
 
122
- - `content`: human-readable Markdown summary.
122
+ - `content`: array of content blocks (human-readable Markdown text plus optional resources).
123
123
  - `structuredContent`: machine-parseable results.
124
124
  - Errors return `structuredContent.ok=false` and an `error` object with `code`, `message`, optional `context` (sanitized, up to 200 chars), `details`, and `recoveryHint`.
125
- - `refine_prompt` and `optimize_prompt` include a `resource` content block with a `file:///` URI containing the refined or optimized prompt.
125
+ - `refine_prompt` and `optimize_prompt` include a `resource` content block with a `file:///` URI and the prompt text in `resource.text` (Markdown).
126
126
 
127
127
  ## Prompts
128
128
 
@@ -159,27 +159,27 @@ Token limits used for `validate_prompt`: `claude` 200000, `gpt` 128000, `gemini`
159
159
 
160
160
  ### Prerequisites
161
161
 
162
- - Node.js >= 20.0.0
162
+ - Node.js >= 22.0.0
163
163
  - npm
164
164
 
165
165
  ### Scripts
166
166
 
167
- | Command | Description |
168
- | ------------------------ | -------------------------------------------------------- |
169
- | `npm run build` | Compile TypeScript and set permissions. |
170
- | `npm run dev` | Run from source in watch mode. |
171
- | `npm run dev:http` | Run from source with `--http` (reserved, no effect). |
172
- | `npm run start` | Run the compiled server from `dist/`. |
173
- | `npm run start:http` | Run compiled server with `--http` (reserved, no effect). |
174
- | `npm run test` | Run Vitest once. |
175
- | `npm run test:coverage` | Run tests with coverage. |
176
- | `npm run test:watch` | Run Vitest in watch mode. |
177
- | `npm run lint` | Run ESLint. |
178
- | `npm run format` | Run Prettier. |
179
- | `npm run type-check` | TypeScript type checking. |
180
- | `npm run inspector` | Run MCP Inspector against `dist/index.js`. |
181
- | `npm run inspector:http` | Inspector with `--http` (reserved, no effect). |
182
- | `npm run duplication` | Run jscpd duplication report. |
167
+ | Command | Description |
168
+ | ------------------------ | ----------------------------------------------------- |
169
+ | `npm run build` | Compile TypeScript and set permissions. |
170
+ | `npm run dev` | Run from source in watch mode. |
171
+ | `npm run dev:http` | Alias of `npm run dev` (no HTTP transport yet). |
172
+ | `npm run start` | Run the compiled server from `dist/`. |
173
+ | `npm run start:http` | Alias of `npm run start` (no HTTP transport yet). |
174
+ | `npm run test` | Run Vitest once. |
175
+ | `npm run test:coverage` | Run tests with coverage. |
176
+ | `npm run test:watch` | Run Vitest in watch mode. |
177
+ | `npm run lint` | Run ESLint. |
178
+ | `npm run format` | Run Prettier. |
179
+ | `npm run type-check` | TypeScript type checking. |
180
+ | `npm run inspector` | Run MCP Inspector against `dist/index.js`. |
181
+ | `npm run inspector:http` | Alias of `npm run inspector` (no HTTP transport yet). |
182
+ | `npm run duplication` | Run jscpd duplication report. |
183
183
 
184
184
  ## Project Structure
185
185
 
@@ -190,8 +190,7 @@ src/
190
190
  config/ Configuration and constants
191
191
  lib/ Shared utilities (LLM, retry, validation)
192
192
  tools/ Tool implementations
193
- prompts/ Prompt templates
194
- resources/ Resource registration (currently none)
193
+ prompts/ MCP prompt templates
195
194
  schemas/ Zod input/output schemas
196
195
  types/ Shared types
197
196
 
@@ -11,7 +11,6 @@ export const SCORING_WEIGHTS = {
11
11
  };
12
12
  export const SERVER_NAME = 'prompttuner-mcp';
13
13
  export const SERVER_VERSION = packageJson.version;
14
- // Configurable via environment variables
15
14
  const { MAX_PROMPT_LENGTH: ENV_MAX_PROMPT_LENGTH, LLM_TIMEOUT_MS: ENV_LLM_TIMEOUT_MS, LLM_MAX_TOKENS: ENV_LLM_MAX_TOKENS, } = config;
16
15
  export const MAX_PROMPT_LENGTH = ENV_MAX_PROMPT_LENGTH;
17
16
  export const MIN_PROMPT_LENGTH = 1;
@@ -11,11 +11,9 @@ const numberString = (def, min = 0) => z
11
11
  .transform((v) => parseInt(v, 10))
12
12
  .refine((n) => n >= min, { message: `Must be >= ${min}` });
13
13
  const envSchema = z.object({
14
- // Server
15
14
  LOG_FORMAT: z.enum(['json', 'text']).optional().default('text'),
16
15
  DEBUG: booleanString,
17
16
  INCLUDE_ERROR_CONTEXT: booleanString,
18
- // LLM
19
17
  LLM_PROVIDER: z
20
18
  .enum(['openai', 'anthropic', 'google'])
21
19
  .optional()
@@ -28,13 +26,10 @@ const envSchema = z.object({
28
26
  .optional()
29
27
  .default('false')
30
28
  .transform((v) => v === 'true'),
31
- // Keys
32
29
  OPENAI_API_KEY: z.string().optional(),
33
30
  ANTHROPIC_API_KEY: z.string().optional(),
34
31
  GOOGLE_API_KEY: z.string().optional(),
35
- // Constraints
36
32
  MAX_PROMPT_LENGTH: numberString(10000, 1),
37
- // Retry
38
33
  RETRY_MAX_ATTEMPTS: numberString(3, 0),
39
34
  RETRY_BASE_DELAY_MS: numberString(1000, 100),
40
35
  RETRY_MAX_DELAY_MS: numberString(10000, 1000),
@@ -1,5 +1,4 @@
1
1
  export declare const PATTERNS: {
2
- readonly roleIndicators: RegExp;
3
2
  readonly exampleIndicators: RegExp;
4
3
  readonly stepByStepIndicators: RegExp;
5
4
  readonly xmlStructure: RegExp;
@@ -8,12 +7,6 @@ export declare const PATTERNS: {
8
7
  readonly claudePatterns: RegExp;
9
8
  readonly gptPatterns: RegExp;
10
9
  readonly vagueWords: RegExp;
11
- readonly needsReasoning: RegExp;
12
- readonly hasStepByStep: RegExp;
13
10
  readonly hasRole: RegExp;
14
- readonly constraintPatterns: RegExp;
15
- readonly outputSpecPatterns: RegExp;
16
11
  readonly fewShotStructure: RegExp;
17
- readonly qualityIndicators: RegExp;
18
- readonly antiPatterns: RegExp;
19
12
  };
@@ -1,5 +1,4 @@
1
1
  export const PATTERNS = {
2
- roleIndicators: /\b(you are|act as|pretend to be|as a|role:|persona:|imagine you are|your role is|you're an?)\b/i,
3
2
  exampleIndicators: /(?:^|\s|[([{])(example:|for example|e\.g\.|such as|here's an example|input:|output:|sample:|demonstration:)(?:$|\s|[)\]}.!?,])/i,
4
3
  stepByStepIndicators: /(?:^|\s|[([{])(step by step|step-by-step|first,|then,|finally,|1\.|2\.|3\.|let's think|let's work through|let's analyze|let's break|systematically)(?:$|\s|[)\]}.!?,])/i,
5
4
  xmlStructure: /<[a-z_]+>[^<]*<\/[a-z_]+>/is,
@@ -8,12 +7,6 @@ export const PATTERNS = {
8
7
  claudePatterns: /<(thinking|response|context|instructions|example|task|requirements|output_format|rules|constraints)>/i,
9
8
  gptPatterns: /^##\s|^###\s|\*\*[^*]+\*\*|^>\s/m,
10
9
  vagueWords: /\b(something|stuff|things|maybe|kind of|sort of|etc|whatever|somehow|certain|various)\b/gi,
11
- needsReasoning: /\b(calculate|analyze|compare|evaluate|explain|solve|debug|review|reason|deduce|derive|prove|assess|investigate|examine|determine)\b/i,
12
- hasStepByStep: /step[- ]by[- ]step|first,|then,|finally,|let's think|let's work through|let's analyze/i,
13
10
  hasRole: /\b(you are|act as|as a|role:|persona:|your role|you're an?|imagine you are)\b/i,
14
- constraintPatterns: /\b(NEVER|ALWAYS|MUST|MUST NOT|DO NOT|RULES:|CONSTRAINTS:|REQUIREMENTS:)\b/,
15
- outputSpecPatterns: /\b(output format|respond with|return as|format:|expected output|response format|<output|## Output)\b/i,
16
11
  fewShotStructure: /<example>|Example \d+:|Input:|Output:|###\s*Example|Q:|A:/i,
17
- qualityIndicators: /\b(specific|detailed|comprehensive|thorough|clear|concise|precise|accurate)\b/i,
18
- antiPatterns: /\b(do whatever|anything|everything|all of it|any way you want)\b/i,
19
12
  };
@@ -1,31 +1,28 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  export type ToolRegistrar = (server: McpServer) => void;
3
- interface TextContentBlock {
4
- type: 'text';
5
- text: string;
6
- }
7
- interface TextResourcePayload {
3
+ interface ResourceTextPayload {
8
4
  uri: string;
9
- mimeType?: string;
10
5
  text: string;
6
+ mimeType?: string;
11
7
  }
12
- interface BlobResourcePayload {
8
+ interface ResourceBlobPayload {
13
9
  uri: string;
14
- mimeType?: string;
15
10
  blob: string;
11
+ mimeType?: string;
16
12
  }
17
- interface ResourceContentBlock {
13
+ export type ContentBlock = {
14
+ type: 'text';
15
+ text: string;
16
+ } | {
18
17
  type: 'resource';
19
- resource: TextResourcePayload | BlobResourcePayload;
20
- }
21
- interface ResourceLinkContentBlock {
18
+ resource: ResourceTextPayload | ResourceBlobPayload;
19
+ } | {
22
20
  type: 'resource_link';
23
21
  uri: string;
24
22
  name: string;
25
23
  description?: string;
26
24
  mimeType?: string;
27
- }
28
- export type ContentBlock = TextContentBlock | ResourceContentBlock | ResourceLinkContentBlock;
25
+ };
29
26
  export interface ErrorResponse {
30
27
  [key: string]: unknown;
31
28
  content: ContentBlock[];
@@ -86,7 +83,6 @@ export interface FormatResult {
86
83
  rawScore: number;
87
84
  }
88
85
  export type LLMProvider = 'openai' | 'anthropic' | 'google';
89
- export type ValidProvider = 'openai' | 'anthropic' | 'google';
90
86
  export interface SafeErrorDetails {
91
87
  status?: number;
92
88
  code?: string;
@@ -113,12 +109,6 @@ export interface LLMToolOptions {
113
109
  timeoutMs?: number;
114
110
  signal?: AbortSignal;
115
111
  }
116
- export interface RetryOptions {
117
- maxRetries?: number;
118
- baseDelayMs?: number;
119
- maxDelayMs?: number;
120
- totalTimeoutMs?: number;
121
- }
122
112
  export interface McpErrorOptions {
123
113
  context?: string;
124
114
  details?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -1,108 +1,69 @@
1
1
  #!/usr/bin/env node
2
2
  import { logger } from './lib/errors.js';
3
- import { createServer, startServer, validateApiKeys } from './server.js';
3
+ import { startServer, validateApiKeys } from './server.js';
4
4
  const SHUTDOWN_DELAY_MS = 500;
5
- const SIGNALS = [
6
- 'SIGHUP',
7
- 'SIGINT',
8
- 'SIGQUIT',
9
- 'SIGILL',
10
- 'SIGTRAP',
11
- 'SIGABRT',
12
- 'SIGBUS',
13
- 'SIGFPE',
14
- 'SIGSEGV',
15
- 'SIGUSR2',
16
- 'SIGTERM',
17
- ];
18
- const ERROR_EVENTS = ['uncaughtException', 'unhandledRejection'];
19
- const EXIT_EVENTS = ['beforeExit'];
20
- let shuttingDown = false;
5
+ const SIGNALS = ['SIGHUP', 'SIGINT', 'SIGTERM'];
21
6
  let server = null;
7
+ let shuttingDown = false;
22
8
  function logSecondShutdown(reason) {
23
- if (reason.signal) {
24
- logger.error({ signal: reason.signal }, `Second ${reason.signal}, exiting`);
25
- return;
26
- }
27
- if (reason.err) {
28
- logger.error({ err: reason.err }, 'Second error, exiting');
29
- return;
30
- }
31
- logger.error('Second shutdown event, exiting');
9
+ logger.error({ reason }, 'Second shutdown, exiting');
32
10
  }
33
- function logShutdownStart(reason) {
34
- if (reason.err) {
35
- logger.error({ err: reason.err }, 'Server closing due to error');
36
- }
37
- if (reason.signal) {
38
- logger.info({ signal: reason.signal }, 'Server shutting down gracefully');
11
+ function logShutdown(reason, err) {
12
+ if (err) {
13
+ logger.error({ err, reason }, 'Server shutting down due to error');
39
14
  }
40
- if (reason.event) {
41
- logger.info({ event: reason.event }, 'Server shutting down gracefully');
15
+ else {
16
+ logger.info({ reason }, 'Server shutting down');
42
17
  }
43
18
  }
44
- function resolveExitCode(reason) {
45
- return reason.err ? 1 : 0;
46
- }
47
19
  function startForcedShutdownTimer() {
48
20
  return setTimeout(() => {
49
21
  logger.error({ delayMs: SHUTDOWN_DELAY_MS }, 'Forced shutdown due to timeout');
50
22
  process.exit(1);
51
23
  }, SHUTDOWN_DELAY_MS);
52
24
  }
53
- async function closeServerIfConnected() {
54
- if (server?.isConnected()) {
25
+ async function closeServer() {
26
+ if (!server?.isConnected())
27
+ return true;
28
+ try {
55
29
  await server.close();
30
+ return true;
31
+ }
32
+ catch (closeError) {
33
+ logger.error({ err: closeError }, 'Error during shutdown');
34
+ return false;
56
35
  }
57
36
  }
58
- async function beginShutdown(reason) {
37
+ async function shutdown(reason, err) {
59
38
  if (shuttingDown) {
60
39
  logSecondShutdown(reason);
61
40
  process.exit(1);
62
41
  return;
63
42
  }
64
43
  shuttingDown = true;
65
- logShutdownStart(reason);
66
- let exitCode = resolveExitCode(reason);
44
+ logShutdown(reason, err);
67
45
  const timeout = startForcedShutdownTimer();
68
- try {
69
- await closeServerIfConnected();
70
- }
71
- catch (error) {
46
+ let exitCode = err ? 1 : 0;
47
+ if (!(await closeServer()))
72
48
  exitCode = 1;
73
- logger.error({ err: error }, 'Error during shutdown');
74
- }
75
- finally {
76
- clearTimeout(timeout);
77
- process.exit(exitCode);
78
- }
49
+ clearTimeout(timeout);
50
+ process.exit(exitCode);
79
51
  }
80
52
  for (const signal of SIGNALS) {
81
- process.once(signal, (received) => {
82
- void beginShutdown({ signal: received });
83
- });
84
- }
85
- for (const event of ERROR_EVENTS) {
86
- process.once(event, (err) => {
87
- void beginShutdown({ err });
88
- });
89
- }
90
- for (const event of EXIT_EVENTS) {
91
- process.once(event, () => {
92
- void beginShutdown({ event });
53
+ process.once(signal, () => {
54
+ void shutdown(signal);
93
55
  });
94
56
  }
95
- process.stdin.once('end', () => {
96
- void beginShutdown({ event: 'stdin_end' });
57
+ process.once('uncaughtException', (err) => {
58
+ void shutdown('uncaughtException', err);
97
59
  });
98
- process.stdin.once('close', () => {
99
- void beginShutdown({ event: 'stdin_close' });
60
+ process.once('unhandledRejection', (err) => {
61
+ void shutdown('unhandledRejection', err);
100
62
  });
101
63
  async function main() {
102
64
  await validateApiKeys();
103
- server = createServer();
104
- await startServer(server);
65
+ server = await startServer();
105
66
  }
106
- main().catch((error) => {
107
- void beginShutdown({ err: error });
67
+ main().catch((err) => {
68
+ void shutdown('startup', err);
108
69
  });
@@ -27,7 +27,6 @@ export class McpError extends Error {
27
27
  this.recoveryHint = recoveryHint;
28
28
  }
29
29
  }
30
- // Custom inspect output for better console debugging
31
30
  [inspect.custom]() {
32
31
  const hint = this.recoveryHint ? ` (Hint: ${this.recoveryHint})` : '';
33
32
  const details = this.details ? ` ${JSON.stringify(this.details)}` : '';
@@ -89,16 +88,13 @@ export function createErrorResponse(error, fallbackCode = ErrorCode.E_LLM_FAILED
89
88
  isError: true,
90
89
  };
91
90
  }
92
- // Sanitizes context by removing API keys and truncating prompts
93
91
  function sanitizeErrorContext(context) {
94
92
  if (!context)
95
93
  return undefined;
96
- // Redact API keys (OpenAI, Anthropic, Google patterns)
97
94
  let sanitized = context
98
95
  .replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_OPENAI_KEY]')
99
96
  .replace(/sk-ant-[A-Za-z0-9_-]{20,}/g, '[REDACTED_ANTHROPIC_KEY]')
100
97
  .replace(/AIza[A-Za-z0-9_-]{35}/g, '[REDACTED_GOOGLE_KEY]');
101
- // Truncate to first 200 chars for privacy
102
98
  const CONTEXT_MAX_LENGTH = 200;
103
99
  if (sanitized.length > CONTEXT_MAX_LENGTH) {
104
100
  sanitized = `${sanitized.slice(0, CONTEXT_MAX_LENGTH)}...`;
@@ -114,7 +110,6 @@ function resolveErrorMessage(error) {
114
110
  function resolveErrorDetails(mcpError) {
115
111
  return mcpError?.details;
116
112
  }
117
- // Default recovery hints for common error codes
118
113
  const DEFAULT_RECOVERY_HINTS = {
119
114
  E_INVALID_INPUT: 'Check the input parameters and try again.',
120
115
  E_LLM_FAILED: 'Retry the request. If the issue persists, check LLM provider status.',
@@ -1,21 +1,14 @@
1
- // Shared utilities for parsing JSON from LLM responses
2
- // Handles common wrapping patterns like ```json code blocks``` and surrounding text.
3
1
  import { LLM_ERROR_PREVIEW_CHARS, LLM_MAX_RESPONSE_LENGTH, } from '../config/constants.js';
4
2
  import { logger, McpError } from './errors.js';
5
3
  import { extractFirstJsonFragment } from './llm-json/scan.js';
6
- // Matches opening code block: ```json or ``` at start (with optional whitespace/newlines before)
7
4
  const CODE_BLOCK_START_RE = /^[\s\n]*```(?:json)?[\s\n]*/i;
8
- // Matches closing code block at end (with optional whitespace/newlines after)
9
5
  const CODE_BLOCK_END_RE = /[\s\n]*```[\s\n]*$/;
10
- // Strips code block markers from the start and end of a string
11
6
  function stripCodeBlockMarkers(text) {
12
7
  let result = text;
13
- // Remove opening code block marker
14
8
  const startMatch = CODE_BLOCK_START_RE.exec(result);
15
9
  if (startMatch) {
16
10
  result = result.slice(startMatch[0].length);
17
11
  }
18
- // Remove closing code block marker
19
12
  const endMatch = CODE_BLOCK_END_RE.exec(result);
20
13
  if (endMatch) {
21
14
  result = result.slice(0, result.length - endMatch[0].length);
@@ -37,8 +30,9 @@ function tryParseJson(jsonText, parse, debugLabel, stageLabel) {
37
30
  return { success: true, value: result };
38
31
  }
39
32
  catch (error) {
40
- logger.debug(`${debugLabel ?? 'JSON parse'}: ${stageLabel} parse failed: ${error instanceof Error ? error.message : String(error)}`);
41
- return { success: false, error };
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ logger.debug(`${debugLabel ?? 'JSON parse'}: ${stageLabel} parse failed: ${message}`);
35
+ return { success: false, error: { stage: stageLabel, message } };
42
36
  }
43
37
  }
44
38
  function logPreviewIfDebug(llmResponseText, maxPreviewChars, contextLabel) {
@@ -46,12 +40,18 @@ function logPreviewIfDebug(llmResponseText, maxPreviewChars, contextLabel) {
46
40
  return;
47
41
  logger.debug({ preview: llmResponseText.slice(0, maxPreviewChars) }, `${contextLabel}: raw response preview`);
48
42
  }
49
- function throwParseFailure(options, llmResponseText, maxPreviewChars) {
43
+ function throwParseFailure(options, llmResponseText, maxPreviewChars, failureDetail) {
50
44
  const contextLabel = options.debugLabel ?? 'LLM response';
51
45
  logPreviewIfDebug(llmResponseText, maxPreviewChars, contextLabel);
52
46
  throw new McpError(options.errorCode, `Failed to parse ${contextLabel} as JSON`, undefined, {
53
47
  strategiesAttempted: ['raw', 'codeblock-stripped', 'extracted-json'],
54
48
  parseFailed: true,
49
+ ...(failureDetail
50
+ ? {
51
+ parseErrorStage: failureDetail.stage,
52
+ parseErrorMessage: failureDetail.message,
53
+ }
54
+ : {}),
55
55
  });
56
56
  }
57
57
  function resolveParseOptions(options) {
@@ -62,45 +62,43 @@ function resolveParseOptions(options) {
62
62
  debugLabel: options.debugLabel,
63
63
  };
64
64
  }
65
- function isJsonStart(text) {
65
+ function shouldTryRawParse(text) {
66
+ if (text.startsWith('```'))
67
+ return false;
66
68
  const firstChar = text[0];
67
69
  return firstChar === '{' || firstChar === '[';
68
70
  }
69
- function shouldTryRawParse(text) {
70
- return !text.startsWith('```') && isJsonStart(text);
71
- }
72
- function attemptParse(payload, label, parse, debugLabel) {
73
- const attempt = tryParseJson(payload, parse, debugLabel, label);
74
- return attempt.success ? attempt.value : null;
75
- }
76
- function parseRawCandidate(text, parse, debugLabel) {
77
- if (!shouldTryRawParse(text))
78
- return null;
79
- return attemptParse(text, 'raw', parse, debugLabel);
80
- }
81
- function parseStrippedCandidate(text, parse, debugLabel) {
71
+ function parseJsonCandidates(text, parse, debugLabel) {
82
72
  const stripped = stripCodeBlockMarkers(text);
83
- if (shouldTryRawParse(text) && stripped === text)
84
- return null;
85
- return attemptParse(stripped, 'stripped markers', parse, debugLabel);
86
- }
87
- function parseExtractedCandidate(text, parse, debugLabel) {
88
73
  const extracted = extractFirstJsonFragment(text);
89
- if (!extracted)
90
- return null;
91
- return attemptParse(extracted, 'extracted fragment', parse, debugLabel);
74
+ const candidates = [
75
+ { label: 'raw', value: shouldTryRawParse(text) ? text : null },
76
+ {
77
+ label: 'stripped markers',
78
+ value: stripped !== text ? stripped : null,
79
+ },
80
+ { label: 'extracted fragment', value: extracted },
81
+ ];
82
+ return parseCandidates(candidates, parse, debugLabel);
92
83
  }
93
- function parseJsonCandidates(text, parse, debugLabel) {
94
- return (parseRawCandidate(text, parse, debugLabel) ??
95
- parseStrippedCandidate(text, parse, debugLabel) ??
96
- parseExtractedCandidate(text, parse, debugLabel));
84
+ function parseCandidates(candidates, parse, debugLabel) {
85
+ let lastError = null;
86
+ for (const candidate of candidates) {
87
+ if (!candidate.value)
88
+ continue;
89
+ const attempt = tryParseJson(candidate.value, parse, debugLabel, candidate.label);
90
+ if (attempt.success)
91
+ return { value: attempt.value };
92
+ lastError = attempt.error;
93
+ }
94
+ return { error: lastError };
97
95
  }
98
96
  export function parseJsonFromLlmResponse(llmResponseText, parse, options) {
99
97
  const parseOptions = resolveParseOptions(options);
100
98
  enforceMaxInputLength(llmResponseText, parseOptions.maxInputLength, parseOptions.errorCode);
101
99
  const jsonStr = llmResponseText.trim();
102
100
  const parsed = parseJsonCandidates(jsonStr, parse, parseOptions.debugLabel);
103
- if (parsed !== null)
104
- return parsed;
105
- return throwParseFailure(parseOptions, llmResponseText, parseOptions.maxPreviewChars);
101
+ if ('value' in parsed)
102
+ return parsed.value;
103
+ return throwParseFailure(parseOptions, llmResponseText, parseOptions.maxPreviewChars, parsed.error);
106
104
  }
@@ -41,7 +41,6 @@ class AnthropicClient {
41
41
  return this.model;
42
42
  }
43
43
  }
44
- // Google safety categories for content filtering
45
44
  const GOOGLE_SAFETY_CATEGORIES = [
46
45
  HarmCategory.HARM_CATEGORY_HATE_SPEECH,
47
46
  HarmCategory.HARM_CATEGORY_HARASSMENT,
@@ -74,8 +73,6 @@ class GoogleClient {
74
73
  return response.trim();
75
74
  }
76
75
  buildSafetySettings() {
77
- // WARNING: Disabling safety settings can expose the application to harmful content.
78
- // Use with caution and only in trusted environments.
79
76
  const disabled = config.GOOGLE_SAFETY_DISABLED;
80
77
  if (this.safetySettingsCache?.disabled === disabled) {
81
78
  return this.safetySettingsCache.settings;