-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjson.ts
More file actions
137 lines (122 loc) · 4.65 KB
/
json.ts
File metadata and controls
137 lines (122 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
/**
* Reserved metadata constant for the MCP JSON payload format version.
* NOT currently emitted in any response payload — callers must not rely on
* it appearing in tool output. Bump this value (with a corresponding breaking
* change note) if a future incompatible JSON shape change is introduced.
*/
export const MCP_JSON_FORMAT_VERSION = "2" as const;
let _cachedVersion: string | undefined;
/** Clears the package version cache (used by tests). */
export function resetReadPackageVersionCache(): void {
_cachedVersion = undefined;
}
export function readPackageVersion(): string {
if (_cachedVersion !== undefined) return _cachedVersion;
const here = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(here, "..", "..", "package.json");
try {
const j = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: string };
_cachedVersion = j.version ?? "0.0.0";
} catch (err) {
console.error(
`[readPackageVersion] Failed to read ${pkgPath}:`,
err instanceof Error ? err.message : String(err),
);
_cachedVersion = "0.0.0";
}
return _cachedVersion;
}
/** FastMCP types require major.minor.patch; strip prerelease suffixes from package.json. */
export function readMcpServerVersion(): `${number}.${number}.${number}` {
const raw = readPackageVersion().trim();
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(raw);
if (m?.[1] !== undefined && m[2] !== undefined && m[3] !== undefined) {
return `${m[1]}.${m[2]}.${m[3]}` as `${number}.${number}.${number}`;
}
return "0.0.0";
}
export function jsonRespond(body: object): string {
return JSON.stringify(body);
}
/**
* Structured error code set used across all MCP tools.
*
* - `AUTH_MISSING`/`AUTH_FAILED`: credential resolution problems.
* - `NOT_FOUND`/`PERMISSION_DENIED`/`RATE_LIMITED`/`VALIDATION`: direct HTTP-status maps.
* - `UPSTREAM_FAILURE`: 5xx, GraphQL errors, or generic GitHub-side failures.
* - Domain codes (`NO_CI_RUNS`, `COMPARE_FAILED`, `LOCAL_REPO_NO_REMOTE`,
* `UNSUPPORTED_LANGUAGE`, `AMBIGUOUS_REPO`): per-tool signals that are not HTTP errors.
* - `INTERNAL`: catch-all for unexpected failures.
*/
export type McpErrorCode =
| "AUTH_MISSING"
| "AUTH_FAILED"
| "NOT_FOUND"
| "PERMISSION_DENIED"
| "RATE_LIMITED"
| "VALIDATION"
| "UPSTREAM_FAILURE"
| "NO_CI_RUNS"
| "COMPARE_FAILED"
| "LOCAL_REPO_NO_REMOTE"
| "UNSUPPORTED_LANGUAGE"
| "AMBIGUOUS_REPO"
| "INTERNAL";
/** Structured error envelope. Returned in the `error` field of JSON responses. */
export interface McpErrorEnvelope {
code: McpErrorCode;
message: string;
retryable: boolean;
suggestedFix?: string;
}
/** Construct an error envelope. `retryable` defaults to `false`. */
export function mkError(
code: McpErrorCode,
message: string,
opts?: { retryable?: boolean; suggestedFix?: string },
): McpErrorEnvelope {
return {
code,
message,
retryable: opts?.retryable ?? false,
...spreadDefined("suggestedFix", opts?.suggestedFix),
};
}
/** Respond with a tool-level error envelope: `{"error": {...}}`. */
export function errorRespond(envelope: McpErrorEnvelope): string {
return jsonRespond({ error: envelope });
}
/** Standard error for a local path whose git `origin` doesn't resolve to GitHub. */
export function mkLocalRepoNoRemote(path: string): McpErrorEnvelope {
return mkError("LOCAL_REPO_NO_REMOTE", `No GitHub origin found for local path ${path}`, {
suggestedFix: "Ensure the path is a git clone with a GitHub `origin` remote.",
});
}
/** Spread into an object literal only when `cond` is true; otherwise `{}`. */
function spreadWhen<T extends Record<string, unknown>>(
cond: boolean,
fields: T,
): T | Record<string, never> {
return cond ? fields : {};
}
/** Spread `{ [key]: value }` only when `value` is not `undefined`. */
export function spreadDefined<K extends string, V>(
key: K,
value: V | undefined,
): Record<K, V> | Record<string, never> {
return spreadWhen(value !== undefined, { [key]: value } as Record<K, V>);
}
/** Truncate text to a maximum number of lines, appending a truncation notice. */
export function truncateLines(text: string, maxLines: number): string {
const lines = text.split("\n");
if (lines.length <= maxLines) return text;
return `${lines.slice(0, maxLines).join("\n")}\n... [${lines.length - maxLines} lines truncated]`;
}
/** Truncate text to a maximum number of characters, appending a truncation notice. */
export function truncateText(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return `${text.slice(0, maxChars)}… [truncated]`;
}