Skip to content

Commit e08696f

Browse files
Albertclaude
andcommitted
refactor: DRY — extract shared responses, health check, config timeout
- Add utils/responses.ts: textContent(), errorResponse(), extractError() eliminates 12x "type: text as const" and 4x error extraction duplication - Add services/health-check.ts: shared checkService() + checkAllServices() used by both system-status tool and system-status resource - Move health check timeout to config.healthCheckTimeoutMs - Simplify all 5 tools and resources to use shared helpers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ffae9a3 commit e08696f

9 files changed

Lines changed: 98 additions & 170 deletions

File tree

src/resources/index.ts

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { getPrompts, getPromptByName } from "../services/prompt-registry.js";
4-
import { config } from "../services/config.js";
5-
import { HttpClient } from "../services/http-client.js";
4+
import { checkAllServices } from "../services/health-check.js";
65

76
export function registerResources(server: McpServer): void {
8-
// Static resource: system status
97
server.registerResource(
108
"system-status",
119
"toad://system/status",
@@ -15,53 +13,30 @@ export function registerResources(server: McpServer): void {
1513
mimeType: "application/json",
1614
},
1715
async (uri) => {
18-
const checks = await Promise.all(
19-
[
20-
{ name: "Semantic Search API", baseUrl: config.semanticSearch.baseUrl },
21-
{ name: "Eval Framework", baseUrl: config.evalFramework.baseUrl },
22-
].map(async ({ name, baseUrl }) => {
23-
const client = new HttpClient({ baseUrl, timeoutMs: 5_000 });
24-
const start = Date.now();
25-
try {
26-
const res = await client.get<unknown>("/health");
27-
return { name, url: baseUrl, healthy: res.ok, latencyMs: Date.now() - start };
28-
} catch (error) {
29-
return {
30-
name,
31-
url: baseUrl,
32-
healthy: false,
33-
latencyMs: Date.now() - start,
34-
error: error instanceof Error ? error.message : String(error),
35-
};
36-
}
37-
}),
38-
);
16+
const status = await checkAllServices();
3917

4018
return {
4119
contents: [
4220
{
4321
uri: uri.href,
44-
text: JSON.stringify({ services: checks, allHealthy: checks.every((c) => c.healthy) }, null, 2),
22+
text: JSON.stringify(status, null, 2),
4523
mimeType: "application/json",
4624
},
4725
],
4826
};
4927
},
5028
);
5129

52-
// Dynamic resource: prompt by name
5330
const promptTemplate = new ResourceTemplate("toad://prompts/{name}", {
54-
list: async () => {
55-
return {
56-
resources: getPrompts().map((p) => ({
57-
uri: `toad://prompts/${p.name}`,
58-
name: p.name,
59-
title: `${p.name} (v${p.version})`,
60-
description: p.description,
61-
mimeType: "application/json",
62-
})),
63-
};
64-
},
31+
list: async () => ({
32+
resources: getPrompts().map((p) => ({
33+
uri: `toad://prompts/${p.name}`,
34+
name: p.name,
35+
title: `${p.name} (v${p.version})`,
36+
description: p.description,
37+
mimeType: "application/json",
38+
})),
39+
}),
6540
});
6641

6742
server.registerResource(
@@ -84,13 +59,7 @@ export function registerResources(server: McpServer): void {
8459
}
8560

8661
return {
87-
contents: [
88-
{
89-
uri: uri.href,
90-
text: JSON.stringify(prompt, null, 2),
91-
mimeType: "application/json",
92-
},
93-
],
62+
contents: [{ uri: uri.href, text: JSON.stringify(prompt, null, 2), mimeType: "application/json" }],
9463
};
9564
},
9665
);

src/services/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export const config = {
55
evalFramework: {
66
baseUrl: process.env.EVAL_FRAMEWORK_URL ?? "http://localhost:3002",
77
},
8+
healthCheckTimeoutMs: 5_000,
89
} as const;

src/services/health-check.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { HttpClient } from "./http-client.js";
2+
import { config } from "./config.js";
3+
import { extractError } from "../utils/responses.js";
4+
5+
export interface HealthCheck {
6+
name: string;
7+
url: string;
8+
healthy: boolean;
9+
latencyMs: number;
10+
error?: string;
11+
}
12+
13+
export async function checkService(name: string, baseUrl: string): Promise<HealthCheck> {
14+
const client = new HttpClient({ baseUrl, timeoutMs: config.healthCheckTimeoutMs });
15+
const start = Date.now();
16+
17+
try {
18+
const response = await client.get<unknown>("/health");
19+
return {
20+
name,
21+
url: baseUrl,
22+
healthy: response.ok,
23+
latencyMs: Date.now() - start,
24+
...(!response.ok && { error: `HTTP ${response.status}` }),
25+
};
26+
} catch (error) {
27+
return {
28+
name,
29+
url: baseUrl,
30+
healthy: false,
31+
latencyMs: Date.now() - start,
32+
error: extractError(error),
33+
};
34+
}
35+
}
36+
37+
export async function checkAllServices(): Promise<{ checks: HealthCheck[]; allHealthy: boolean }> {
38+
const checks = await Promise.all([
39+
checkService("Semantic Search API", config.semanticSearch.baseUrl),
40+
checkService("Eval Framework", config.evalFramework.baseUrl),
41+
]);
42+
43+
return { checks, allHealthy: checks.every((c) => c.healthy) };
44+
}

src/tools/get-prompt.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod/v4";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { getPromptByName } from "../services/prompt-registry.js";
4+
import { textContent, errorResponse } from "../utils/responses.js";
45

56
export function registerGetPromptTool(server: McpServer): void {
67
server.tool(
@@ -23,21 +24,11 @@ export function registerGetPromptTool(server: McpServer): void {
2324
const prompt = getPromptByName(name);
2425

2526
if (!prompt) {
26-
return {
27-
isError: true,
28-
content: [
29-
{
30-
type: "text" as const,
31-
text: `Prompt "${name}" not found. Use toad_list_prompts to see available prompts.`,
32-
},
33-
],
34-
};
27+
return errorResponse(`Prompt "${name}" not found. Use toad_list_prompts to see available prompts.`);
3528
}
3629

3730
if (response_format === "json") {
38-
return {
39-
content: [{ type: "text" as const, text: JSON.stringify(prompt, null, 2) }],
40-
};
31+
return textContent(JSON.stringify(prompt, null, 2));
4132
}
4233

4334
const scoreHistory = prompt.scoreHistory.length
@@ -61,9 +52,7 @@ export function registerGetPromptTool(server: McpServer): void {
6152
scoreHistory,
6253
];
6354

64-
return {
65-
content: [{ type: "text" as const, text: lines.join("\n") }],
66-
};
55+
return textContent(lines.join("\n"));
6756
},
6857
);
6958
}

src/tools/list-prompts.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod/v4";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { getPrompts } from "../services/prompt-registry.js";
4+
import { textContent } from "../utils/responses.js";
45

56
export function registerListPromptsTool(server: McpServer): void {
67
server.tool(
@@ -29,14 +30,7 @@ export function registerListPromptsTool(server: McpServer): void {
2930
const hasMore = offset + limit < total;
3031

3132
if (response_format === "json") {
32-
return {
33-
content: [
34-
{
35-
type: "text" as const,
36-
text: JSON.stringify({ prompts: page, total, offset, limit, has_more: hasMore }, null, 2),
37-
},
38-
],
39-
};
33+
return textContent(JSON.stringify({ prompts: page, total, offset, limit, has_more: hasMore }, null, 2));
4034
}
4135

4236
const lines = [
@@ -50,9 +44,7 @@ export function registerListPromptsTool(server: McpServer): void {
5044
hasMore ? `_More results available. Use offset=${offset + limit} to continue._` : "_End of list._",
5145
];
5246

53-
return {
54-
content: [{ type: "text" as const, text: lines.join("\n") }],
55-
};
47+
return textContent(lines.join("\n"));
5648
},
5749
);
5850
}

src/tools/run-eval.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod/v4";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { HttpClient } from "../services/http-client.js";
44
import { config } from "../services/config.js";
5+
import { textContent, errorResponse, extractError } from "../utils/responses.js";
56

67
interface EvalResult {
78
suite: string;
@@ -36,21 +37,12 @@ export function registerRunEvalTool(server: McpServer): void {
3637
},
3738
async ({ suite, variant }) => {
3839
try {
39-
const response = await httpClient.post<EvalResult>("/api/eval/run", {
40-
suite,
41-
variant,
42-
});
40+
const response = await httpClient.post<EvalResult>("/api/eval/run", { suite, variant });
4341

4442
if (!response.ok) {
45-
return {
46-
isError: true,
47-
content: [
48-
{
49-
type: "text" as const,
50-
text: `Eval Framework returned ${response.status}. Ensure the service is running at ${config.evalFramework.baseUrl}`,
51-
},
52-
],
53-
};
43+
return errorResponse(
44+
`Eval Framework returned ${response.status}. Ensure the service is running at ${config.evalFramework.baseUrl}`,
45+
);
5446
}
5547

5648
const { score, passed, failed, total, details } = response.data;
@@ -68,20 +60,11 @@ export function registerRunEvalTool(server: McpServer): void {
6860
),
6961
].join("\n");
7062

71-
return {
72-
content: [{ type: "text" as const, text: summary }],
73-
};
63+
return textContent(summary);
7464
} catch (error) {
75-
const message = error instanceof Error ? error.message : String(error);
76-
return {
77-
isError: true,
78-
content: [
79-
{
80-
type: "text" as const,
81-
text: `Failed to reach Eval Framework at ${config.evalFramework.baseUrl}: ${message}`,
82-
},
83-
],
84-
};
65+
return errorResponse(
66+
`Failed to reach Eval Framework at ${config.evalFramework.baseUrl}: ${extractError(error)}`,
67+
);
8568
}
8669
},
8770
);

src/tools/search-documents.ts

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod/v4";
22
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { HttpClient } from "../services/http-client.js";
44
import { config } from "../services/config.js";
5+
import { textContent, errorResponse, extractError } from "../utils/responses.js";
56

67
interface SearchResult {
78
id: string;
@@ -33,21 +34,12 @@ export function registerSearchDocumentsTool(server: McpServer): void {
3334
},
3435
async ({ query, limit }) => {
3536
try {
36-
const response = await httpClient.post<SearchResponse>("/api/search", {
37-
query,
38-
limit,
39-
});
37+
const response = await httpClient.post<SearchResponse>("/api/search", { query, limit });
4038

4139
if (!response.ok) {
42-
return {
43-
isError: true,
44-
content: [
45-
{
46-
type: "text" as const,
47-
text: `Semantic Search API returned ${response.status}. Ensure the service is running at ${config.semanticSearch.baseUrl}`,
48-
},
49-
],
50-
};
40+
return errorResponse(
41+
`Semantic Search API returned ${response.status}. Ensure the service is running at ${config.semanticSearch.baseUrl}`,
42+
);
5143
}
5244

5345
const { results, total } = response.data;
@@ -59,25 +51,11 @@ export function registerSearchDocumentsTool(server: McpServer): void {
5951
)
6052
.join("\n\n---\n\n");
6153

62-
return {
63-
content: [
64-
{
65-
type: "text" as const,
66-
text: `Found ${total} results for "${query}" (showing ${results.length}):\n\n${formatted}`,
67-
},
68-
],
69-
};
54+
return textContent(`Found ${total} results for "${query}" (showing ${results.length}):\n\n${formatted}`);
7055
} catch (error) {
71-
const message = error instanceof Error ? error.message : String(error);
72-
return {
73-
isError: true,
74-
content: [
75-
{
76-
type: "text" as const,
77-
text: `Failed to reach Semantic Search API at ${config.semanticSearch.baseUrl}: ${message}`,
78-
},
79-
],
80-
};
56+
return errorResponse(
57+
`Failed to reach Semantic Search API at ${config.semanticSearch.baseUrl}: ${extractError(error)}`,
58+
);
8159
}
8260
},
8361
);

0 commit comments

Comments
 (0)