@esaio/esa-mcp-server 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 (80) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.dockerignore +36 -0
  3. package/.envrc +2 -0
  4. package/.github/dependabot.yml +18 -0
  5. package/.github/workflows/docker-publish.yml +120 -0
  6. package/.github/workflows/main.yml +41 -0
  7. package/.node-version +1 -0
  8. package/CLAUDE.md +94 -0
  9. package/Dockerfile +34 -0
  10. package/LICENSE +7 -0
  11. package/README.en.md +139 -0
  12. package/README.md +139 -0
  13. package/bin/index.js +30 -0
  14. package/biome.json +57 -0
  15. package/package.json +48 -0
  16. package/src/__tests__/fixtures/mock-comment.ts +90 -0
  17. package/src/__tests__/fixtures/mock-post.ts +79 -0
  18. package/src/__tests__/index.test.ts +209 -0
  19. package/src/api_client/__tests__/index.test.ts +149 -0
  20. package/src/api_client/__tests__/middleware.test.ts +119 -0
  21. package/src/api_client/__tests__/with-context.test.ts +98 -0
  22. package/src/api_client/index.ts +29 -0
  23. package/src/api_client/middleware.ts +21 -0
  24. package/src/api_client/with-context.ts +26 -0
  25. package/src/config/__tests__/index.test.ts +65 -0
  26. package/src/config/index.ts +20 -0
  27. package/src/context/mcp-context.ts +1 -0
  28. package/src/context/stdio-context.ts +6 -0
  29. package/src/errors/missing-team-name-error.ts +8 -0
  30. package/src/formatters/__tests__/mcp-response.test.ts +106 -0
  31. package/src/formatters/mcp-response.ts +95 -0
  32. package/src/generated/api-types.ts +2691 -0
  33. package/src/i18n/__tests__/index.test.ts +53 -0
  34. package/src/i18n/index.ts +39 -0
  35. package/src/index.ts +47 -0
  36. package/src/locales/en.json +13 -0
  37. package/src/locales/ja.json +13 -0
  38. package/src/prompts/__tests__/index.test.ts +47 -0
  39. package/src/prompts/__tests__/summarize-post.test.ts +291 -0
  40. package/src/prompts/index.ts +21 -0
  41. package/src/prompts/summarize-post.ts +94 -0
  42. package/src/resources/__tests__/index.test.ts +49 -0
  43. package/src/resources/__tests__/recent-posts-list.test.ts +91 -0
  44. package/src/resources/__tests__/recent-posts.test.ts +270 -0
  45. package/src/resources/index.ts +33 -0
  46. package/src/resources/recent-posts-list.ts +22 -0
  47. package/src/resources/recent-posts.ts +45 -0
  48. package/src/schemas/team-name-schema.ts +19 -0
  49. package/src/tools/__tests__/categories.test.ts +226 -0
  50. package/src/tools/__tests__/comments.test.ts +970 -0
  51. package/src/tools/__tests__/helps.test.ts +222 -0
  52. package/src/tools/__tests__/index.test.ts +47 -0
  53. package/src/tools/__tests__/post-actions.test.ts +445 -0
  54. package/src/tools/__tests__/posts.test.ts +917 -0
  55. package/src/tools/__tests__/search.test.ts +339 -0
  56. package/src/tools/__tests__/teams.test.ts +615 -0
  57. package/src/tools/categories.ts +93 -0
  58. package/src/tools/comments.ts +258 -0
  59. package/src/tools/helps.ts +50 -0
  60. package/src/tools/index.ts +324 -0
  61. package/src/tools/post-actions.ts +132 -0
  62. package/src/tools/posts.ts +179 -0
  63. package/src/tools/search.ts +98 -0
  64. package/src/tools/teams.ts +157 -0
  65. package/src/transformers/__tests__/category-transformer.test.ts +161 -0
  66. package/src/transformers/__tests__/comment-transformer.test.ts +129 -0
  67. package/src/transformers/__tests__/post-name-normalizer.test.ts +53 -0
  68. package/src/transformers/__tests__/post-transformer.test.ts +70 -0
  69. package/src/transformers/__tests__/query-normalizer.test.ts +98 -0
  70. package/src/transformers/__tests__/team-name-normalizer.test.ts +21 -0
  71. package/src/transformers/category-transformer.ts +36 -0
  72. package/src/transformers/comment-transformer.ts +34 -0
  73. package/src/transformers/post-name-normalizer.ts +30 -0
  74. package/src/transformers/post-transformer.ts +38 -0
  75. package/src/transformers/query-normalizer.ts +36 -0
  76. package/src/transformers/team-name-normalizer.ts +7 -0
  77. package/tsconfig.build.json +4 -0
  78. package/tsconfig.json +30 -0
  79. package/tsdown.config.ts +13 -0
  80. package/vitest.config.ts +24 -0
@@ -0,0 +1,149 @@
1
+ import createClient from "openapi-fetch";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import packageJson from "../../../package.json" with { type: "json" };
4
+ import type { paths } from "../../generated/api-types.js";
5
+ import { createEsaClient } from "../index.js";
6
+
7
+ // Mock openapi-fetch to test client configuration without actual HTTP calls
8
+ vi.mock("openapi-fetch", () => ({
9
+ default: vi.fn(),
10
+ }));
11
+
12
+ describe("createEsaClient", () => {
13
+ // Create mock client with use method to verify middleware registration
14
+ const createMockClient = () =>
15
+ ({
16
+ use: vi.fn(),
17
+ }) as unknown as ReturnType<typeof createClient<paths>>;
18
+
19
+ // Setup test environment with configurable parameters for different test scenarios
20
+ const setupTest = (overrides?: {
21
+ apiBaseUrl?: string;
22
+ apiAccessToken?: string;
23
+ }) => {
24
+ const config = {
25
+ apiBaseUrl: overrides?.apiBaseUrl ?? "https://api.esa.example.com",
26
+ apiAccessToken: overrides?.apiAccessToken ?? "test-token",
27
+ };
28
+
29
+ const mockClient = createMockClient();
30
+ const mockCreateClient = vi.mocked(createClient);
31
+ mockCreateClient.mockReturnValue(mockClient);
32
+
33
+ return { config, mockClient, mockCreateClient };
34
+ };
35
+
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ });
39
+
40
+ it("should create client with correct baseUrl", () => {
41
+ const { config, mockCreateClient } = setupTest();
42
+
43
+ createEsaClient(config.apiAccessToken, config.apiBaseUrl);
44
+
45
+ expect(mockCreateClient).toHaveBeenCalledWith({
46
+ baseUrl: config.apiBaseUrl,
47
+ });
48
+ });
49
+
50
+ it("should add User-Agent and Auth middleware to client", () => {
51
+ const { config, mockClient } = setupTest();
52
+
53
+ createEsaClient(config.apiAccessToken, config.apiBaseUrl);
54
+
55
+ expect(mockClient.use).toHaveBeenCalledTimes(2);
56
+ expect(mockClient.use).toHaveBeenNthCalledWith(
57
+ 1,
58
+ expect.objectContaining({
59
+ onRequest: expect.any(Function),
60
+ }),
61
+ );
62
+ expect(mockClient.use).toHaveBeenNthCalledWith(
63
+ 2,
64
+ expect.objectContaining({
65
+ onRequest: expect.any(Function),
66
+ }),
67
+ );
68
+ });
69
+
70
+ it("should return the configured client", () => {
71
+ const { config, mockClient } = setupTest();
72
+
73
+ const result = createEsaClient(config.apiAccessToken, config.apiBaseUrl);
74
+
75
+ expect(result).toBe(mockClient);
76
+ });
77
+
78
+ it("should set User-Agent header in middleware", async () => {
79
+ const { config, mockClient } = setupTest();
80
+
81
+ createEsaClient(config.apiAccessToken, config.apiBaseUrl);
82
+
83
+ // Get the User-Agent middleware function (first one registered)
84
+ const useMock = mockClient.use as ReturnType<typeof vi.fn>;
85
+ const userAgentMiddleware = useMock.mock.calls[0][0];
86
+
87
+ // Create mock request to verify User-Agent header is set
88
+ const mockRequest = {
89
+ headers: {
90
+ set: vi.fn(),
91
+ },
92
+ };
93
+
94
+ await userAgentMiddleware.onRequest({ request: mockRequest });
95
+
96
+ expect(mockRequest.headers.set).toHaveBeenCalledWith(
97
+ "User-Agent",
98
+ `esa-mcp-server/${packageJson.version} (official)`,
99
+ );
100
+ });
101
+
102
+ it("should set Authorization header in middleware", async () => {
103
+ const { config, mockClient } = setupTest();
104
+
105
+ createEsaClient(config.apiAccessToken, config.apiBaseUrl);
106
+
107
+ // Get the Auth middleware function (second one registered)
108
+ const useMock = mockClient.use as ReturnType<typeof vi.fn>;
109
+ const authMiddleware = useMock.mock.calls[1][0];
110
+
111
+ // Create mock request to verify Authorization header is set
112
+ const mockRequest = {
113
+ headers: {
114
+ set: vi.fn(),
115
+ },
116
+ };
117
+
118
+ await authMiddleware.onRequest({ request: mockRequest });
119
+
120
+ expect(mockRequest.headers.set).toHaveBeenCalledWith(
121
+ "Authorization",
122
+ `Bearer ${config.apiAccessToken}`,
123
+ );
124
+ });
125
+
126
+ it("should create separate client instances for different tokens", () => {
127
+ const { config, mockCreateClient, mockClient } = setupTest();
128
+ const token1 = "token-1";
129
+ const token2 = "token-2";
130
+
131
+ createEsaClient(token1, config.apiBaseUrl);
132
+ createEsaClient(token2, config.apiBaseUrl);
133
+
134
+ expect(mockCreateClient).toHaveBeenCalledTimes(2);
135
+ expect(mockClient.use).toHaveBeenCalledTimes(4); // 2 middlewares × 2 clients
136
+ });
137
+
138
+ it("should create separate client instances for different base URLs", () => {
139
+ const { config, mockCreateClient } = setupTest();
140
+ const url1 = "https://api.esa.example.com";
141
+ const url2 = "https://api.example.com";
142
+
143
+ createEsaClient(config.apiAccessToken, url1);
144
+ createEsaClient(config.apiAccessToken, url2);
145
+
146
+ expect(mockCreateClient).toHaveBeenCalledWith({ baseUrl: url1 });
147
+ expect(mockCreateClient).toHaveBeenCalledWith({ baseUrl: url2 });
148
+ });
149
+ });
@@ -0,0 +1,119 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createAuthMiddleware } from "../middleware.js";
3
+
4
+ describe("createAuthMiddleware", () => {
5
+ // Mock console.error to verify rate limit and error logging without console output
6
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
7
+
8
+ // Create test request for authorization header testing
9
+ const createTestRequest = (url = "https://api.esa.example.com/test") =>
10
+ new Request(url);
11
+
12
+ // Create test response with optional rate limit headers
13
+ const createTestResponse = (rateLimitHeaders?: {
14
+ limit?: string;
15
+ remaining?: string;
16
+ }) => {
17
+ const headers: Record<string, string> = {};
18
+ if (rateLimitHeaders?.limit) {
19
+ headers["x-ratelimit-limit"] = rateLimitHeaders.limit;
20
+ }
21
+ if (rateLimitHeaders?.remaining) {
22
+ headers["x-ratelimit-remaining"] = rateLimitHeaders.remaining;
23
+ }
24
+ return new Response(null, { headers });
25
+ };
26
+
27
+ // Helper to call middleware methods with minimal required context using as unknown as pattern
28
+ const callOnRequest = (
29
+ middleware: ReturnType<typeof createAuthMiddleware>,
30
+ request: Request,
31
+ ) =>
32
+ middleware.onRequest?.({
33
+ request,
34
+ schemaPath: "/test",
35
+ params: {},
36
+ id: "test-id",
37
+ options: { baseUrl: "https://api.esa.example.com" },
38
+ } as unknown as Parameters<NonNullable<typeof middleware.onRequest>>[0]);
39
+
40
+ const callOnResponse = (
41
+ middleware: ReturnType<typeof createAuthMiddleware>,
42
+ response: Response,
43
+ ) =>
44
+ middleware.onResponse?.({
45
+ request: createTestRequest(),
46
+ response,
47
+ schemaPath: "/test",
48
+ params: {},
49
+ id: "test-id",
50
+ options: { baseUrl: "https://api.esa.example.com" },
51
+ } as unknown as Parameters<NonNullable<typeof middleware.onResponse>>[0]);
52
+
53
+ const callOnError = (
54
+ middleware: ReturnType<typeof createAuthMiddleware>,
55
+ error: Error,
56
+ ) =>
57
+ middleware.onError?.({
58
+ request: createTestRequest(),
59
+ error,
60
+ schemaPath: "/test",
61
+ params: {},
62
+ id: "test-id",
63
+ options: { baseUrl: "https://api.esa.example.com" },
64
+ } as unknown as Parameters<NonNullable<typeof middleware.onError>>[0]);
65
+
66
+ beforeEach(() => {
67
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
68
+ });
69
+
70
+ afterEach(() => {
71
+ consoleErrorSpy.mockRestore();
72
+ });
73
+
74
+ it("should add authorization header", async () => {
75
+ const middleware = createAuthMiddleware("test-token");
76
+ const request = createTestRequest();
77
+
78
+ expect(middleware.onRequest).toBeDefined();
79
+ expect(request.headers.get("Authorization")).toBeNull();
80
+
81
+ await callOnRequest(middleware, request);
82
+
83
+ expect(request.headers.get("Authorization")).toBe("Bearer test-token");
84
+ });
85
+
86
+ it("should log rate limit info when present", async () => {
87
+ const middleware = createAuthMiddleware("test-token");
88
+ const response = createTestResponse({ limit: "75", remaining: "50" });
89
+
90
+ await callOnResponse(middleware, response);
91
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Rate limit: 50/75");
92
+ });
93
+
94
+ it("should handle response without rate limit headers", async () => {
95
+ const middleware = createAuthMiddleware("test-token");
96
+ const response = createTestResponse();
97
+
98
+ await callOnResponse(middleware, response);
99
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("should log network errors", async () => {
103
+ const middleware = createAuthMiddleware("test-token");
104
+ const error = new Error("Network failed");
105
+
106
+ await callOnError(middleware, error);
107
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Network Error:", error);
108
+ });
109
+
110
+ it("should return the modified request", async () => {
111
+ const middleware = createAuthMiddleware("test-token");
112
+ const request = createTestRequest();
113
+
114
+ const result = await callOnRequest(middleware, request);
115
+
116
+ expect(result).toBe(request);
117
+ expect(request.headers.get("Authorization")).toBe("Bearer test-token");
118
+ });
119
+ });
@@ -0,0 +1,98 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createEsaClient } from "../index.js";
3
+ import { withContext } from "../with-context.js";
4
+
5
+ // Mock createEsaClient to test context handling without real API client creation
6
+ vi.mock("../index.js", () => ({
7
+ createEsaClient: vi.fn(),
8
+ }));
9
+
10
+ describe("withContext", () => {
11
+ const mockCreateEsaClient = vi.mocked(createEsaClient);
12
+
13
+ // Mock client instance to verify it's passed to handlers correctly
14
+ const mockClient: Partial<ReturnType<typeof createEsaClient>> = {};
15
+
16
+ // Mock handler function to verify arguments and return values
17
+ const mockHandler = vi.fn();
18
+
19
+ // Create test context with default values and optional overrides
20
+ const createTestContext = (overrides?: {
21
+ apiAccessToken?: string;
22
+ apiBaseUrl?: string;
23
+ }) => ({
24
+ apiAccessToken: overrides?.apiAccessToken ?? "test-token",
25
+ apiBaseUrl: overrides?.apiBaseUrl ?? "https://api.esa.example.com",
26
+ });
27
+
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ mockCreateEsaClient.mockReturnValue(
31
+ mockClient as ReturnType<typeof createEsaClient>,
32
+ );
33
+ });
34
+
35
+ it("should create client and call handler for StdioContext", async () => {
36
+ const context = createTestContext();
37
+ const expectedResult = { success: true };
38
+ mockHandler.mockResolvedValue(expectedResult);
39
+
40
+ const result = await withContext(context, mockHandler, "arg1", "arg2");
41
+
42
+ expect(mockCreateEsaClient).toHaveBeenCalledWith(
43
+ context.apiAccessToken,
44
+ context.apiBaseUrl,
45
+ );
46
+ expect(mockHandler).toHaveBeenCalledWith(mockClient, "arg1", "arg2");
47
+ expect(result).toBe(expectedResult);
48
+ });
49
+
50
+ it("should work with different apiBaseUrl", async () => {
51
+ const context = createTestContext({
52
+ apiBaseUrl: "https://api.example.com",
53
+ });
54
+
55
+ await withContext(context, mockHandler);
56
+
57
+ expect(mockCreateEsaClient).toHaveBeenCalledWith(
58
+ context.apiAccessToken,
59
+ context.apiBaseUrl,
60
+ );
61
+ });
62
+
63
+ it("should throw error for unsupported context type", async () => {
64
+ // Create invalid context missing required apiAccessToken property
65
+ const invalidContext = {
66
+ someOtherProperty: "value",
67
+ };
68
+
69
+ await expect(
70
+ withContext(
71
+ invalidContext as unknown as Parameters<typeof withContext>[0],
72
+ mockHandler,
73
+ ),
74
+ ).rejects.toThrow(
75
+ "Unsupported context type. Only StdioContext is currently supported.",
76
+ );
77
+ });
78
+
79
+ it("should handle handler errors correctly", async () => {
80
+ const context = createTestContext();
81
+ const handlerError = new Error("Handler failed");
82
+ mockHandler.mockRejectedValue(handlerError);
83
+
84
+ await expect(withContext(context, mockHandler)).rejects.toThrow(
85
+ "Handler failed",
86
+ );
87
+ });
88
+
89
+ it("should pass through handler arguments correctly", async () => {
90
+ const context = createTestContext();
91
+ const args = [{ page: 1 }, { per_page: 20 }, "extra"];
92
+ mockHandler.mockResolvedValue("success");
93
+
94
+ await withContext(context, mockHandler, ...args);
95
+
96
+ expect(mockHandler).toHaveBeenCalledWith(mockClient, ...args);
97
+ });
98
+ });
@@ -0,0 +1,29 @@
1
+ import createClient from "openapi-fetch";
2
+ import packageJson from "../../package.json" with { type: "json" };
3
+ import type { paths } from "../generated/api-types.js";
4
+ import { createAuthMiddleware } from "./middleware.js";
5
+
6
+ const packageVersion = packageJson.version;
7
+
8
+ function createUserAgentMiddleware(version: string) {
9
+ return {
10
+ async onRequest({ request }: { request: Request }) {
11
+ request.headers.set("User-Agent", `esa-mcp-server/${version} (official)`);
12
+ return request;
13
+ },
14
+ };
15
+ }
16
+
17
+ export function createEsaClient(
18
+ apiAccessToken: string,
19
+ apiBaseUrl: string = "https://api.esa.io",
20
+ ) {
21
+ const client = createClient<paths>({
22
+ baseUrl: apiBaseUrl,
23
+ });
24
+
25
+ client.use(createUserAgentMiddleware(packageVersion));
26
+ client.use(createAuthMiddleware(apiAccessToken));
27
+
28
+ return client;
29
+ }
@@ -0,0 +1,21 @@
1
+ import type { Middleware } from "openapi-fetch";
2
+
3
+ export function createAuthMiddleware(apiAccessToken: string): Middleware {
4
+ return {
5
+ async onRequest({ request }) {
6
+ request.headers.set("Authorization", `Bearer ${apiAccessToken}`);
7
+ return request;
8
+ },
9
+ async onResponse({ response }) {
10
+ const rateLimit = response.headers.get("x-ratelimit-limit");
11
+ const remaining = response.headers.get("x-ratelimit-remaining");
12
+ if (rateLimit && remaining) {
13
+ console.error(`Rate limit: ${remaining}/${rateLimit}`);
14
+ }
15
+ return response;
16
+ },
17
+ async onError({ error }) {
18
+ console.error("Network Error:", error);
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,26 @@
1
+ import { createEsaClient } from "../api_client/index.js";
2
+ import type { MCPContext } from "../context/mcp-context.js";
3
+
4
+ export async function withContext<T extends unknown[], R>(
5
+ context: MCPContext,
6
+ handler: (
7
+ client: ReturnType<typeof createEsaClient>,
8
+ ...args: T
9
+ ) => Promise<R>,
10
+ ...args: T
11
+ ): Promise<R> {
12
+ let client: ReturnType<typeof createEsaClient>;
13
+
14
+ if ("apiAccessToken" in context && "apiBaseUrl" in context) {
15
+ client = createEsaClient(
16
+ context.apiAccessToken as string,
17
+ context.apiBaseUrl as string,
18
+ );
19
+ } else {
20
+ throw new Error(
21
+ "Unsupported context type. Only StdioContext is currently supported.",
22
+ );
23
+ }
24
+
25
+ return handler(client, ...args);
26
+ }
@@ -0,0 +1,65 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import packageJson from "../../../package.json" with { type: "json" };
3
+
4
+ describe("config", () => {
5
+ beforeEach(() => {
6
+ vi.resetModules();
7
+ vi.stubEnv("ESA_ACCESS_TOKEN", undefined);
8
+ vi.stubEnv("ESA_API_BASE_URL", undefined);
9
+ });
10
+
11
+ afterEach(() => {
12
+ vi.unstubAllEnvs();
13
+ });
14
+
15
+ describe("config object", () => {
16
+ it("should have correct default values", async () => {
17
+ const { config } = await import("../index.js");
18
+
19
+ expect(config.esa.apiBaseUrl).toBe("https://api.esa.io");
20
+ expect(config.server.name).toBe("esa-mcp-server");
21
+ expect(config.server.version).toBe(packageJson.version);
22
+ expect(config.server.description).toBe("Official MCP server for esa.io");
23
+ });
24
+
25
+ it("should use environment variables when set", async () => {
26
+ vi.stubEnv("ESA_ACCESS_TOKEN", "test-token");
27
+ vi.stubEnv("ESA_API_BASE_URL", "https://api.esa.localhost");
28
+
29
+ const { config } = await import("../index.js");
30
+
31
+ expect(config.esa.apiAccessToken).toBe("test-token");
32
+ expect(config.esa.apiBaseUrl).toBe("https://api.esa.localhost");
33
+ });
34
+ });
35
+
36
+ describe("validateConfig", () => {
37
+ it("should throw error when ESA_ACCESS_TOKEN is not set", async () => {
38
+ vi.stubEnv("ESA_ACCESS_TOKEN", undefined);
39
+
40
+ const { validateConfig } = await import("../index.js");
41
+
42
+ expect(() => validateConfig()).toThrow(
43
+ "ESA_ACCESS_TOKEN environment variable is required",
44
+ );
45
+ });
46
+
47
+ it("should not throw error when ESA_ACCESS_TOKEN is set", async () => {
48
+ vi.stubEnv("ESA_ACCESS_TOKEN", "valid-token");
49
+
50
+ const { validateConfig } = await import("../index.js");
51
+
52
+ expect(() => validateConfig()).not.toThrow();
53
+ });
54
+
55
+ it("should throw error when ESA_ACCESS_TOKEN is empty string", async () => {
56
+ vi.stubEnv("ESA_ACCESS_TOKEN", "");
57
+
58
+ const { validateConfig } = await import("../index.js");
59
+
60
+ expect(() => validateConfig()).toThrow(
61
+ "ESA_ACCESS_TOKEN environment variable is required",
62
+ );
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,20 @@
1
+ import packageJson from "../../package.json" with { type: "json" };
2
+ import type { StdioContext } from "../context/stdio-context.js";
3
+
4
+ export const config = {
5
+ esa: {
6
+ apiAccessToken: process.env.ESA_ACCESS_TOKEN || "",
7
+ apiBaseUrl: process.env.ESA_API_BASE_URL || "https://api.esa.io",
8
+ } satisfies StdioContext,
9
+ server: {
10
+ name: "esa-mcp-server",
11
+ version: packageJson.version,
12
+ description: "Official MCP server for esa.io",
13
+ },
14
+ } as const;
15
+
16
+ export function validateConfig(): void {
17
+ if (!config.esa.apiAccessToken) {
18
+ throw new Error("ESA_ACCESS_TOKEN environment variable is required");
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export type MCPContext = Record<string, unknown>;
@@ -0,0 +1,6 @@
1
+ import type { MCPContext } from "./mcp-context.js";
2
+
3
+ export interface StdioContext extends MCPContext {
4
+ apiAccessToken: string;
5
+ apiBaseUrl: string;
6
+ }
@@ -0,0 +1,8 @@
1
+ export class MissingTeamNameError extends Error {
2
+ constructor() {
3
+ super(
4
+ "Missing required parameter 'teamName'. Use esa_get_teams to list available teams, then retry with teamName specified.",
5
+ );
6
+ this.name = "MissingTeamNameError";
7
+ }
8
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ formatPromptError,
4
+ formatPromptResponse,
5
+ formatResourceError,
6
+ formatResourceResponse,
7
+ formatToolError,
8
+ formatToolResponse,
9
+ } from "../mcp-response.js";
10
+
11
+ describe("response formatters", () => {
12
+ describe("formatToolResponse", () => {
13
+ it("should format data as JSON text content", () => {
14
+ const data = { key: "value", count: 42 };
15
+ const result = formatToolResponse(data);
16
+
17
+ expect(result.content[0].type).toBe("text");
18
+ expect(result.content[0].text).toBe(JSON.stringify(data, null, 2));
19
+ });
20
+ });
21
+
22
+ describe("formatResourceResponse", () => {
23
+ it("should format data as JSON resource with URI and mimeType", () => {
24
+ const data = { resource: "data" };
25
+ const uri = "esa://resource/123";
26
+ const result = formatResourceResponse(data, uri);
27
+
28
+ expect(result.contents[0]).toEqual({
29
+ uri,
30
+ mimeType: "application/json",
31
+ text: JSON.stringify(data, null, 2),
32
+ });
33
+ });
34
+ });
35
+
36
+ describe("formatPromptResponse", () => {
37
+ it("should format message as user role content", () => {
38
+ const message = "This is a prompt message";
39
+ const result = formatPromptResponse(message);
40
+
41
+ expect(result.messages[0]).toEqual({
42
+ role: "user",
43
+ content: {
44
+ type: "text",
45
+ text: message,
46
+ },
47
+ });
48
+ });
49
+ });
50
+ });
51
+
52
+ describe("error formatters", () => {
53
+ it("should format Error instances consistently", () => {
54
+ const error = new Error("test error");
55
+
56
+ expect(formatToolError(error).content[0].text).toBe("Error: test error");
57
+ expect(formatResourceError(error, "uri").contents[0].text).toBe(
58
+ "Error: test error",
59
+ );
60
+ expect(formatPromptError(error).messages[0].content.text).toBe(
61
+ "Error: test error",
62
+ );
63
+ });
64
+
65
+ it("should format status codes as API responses", () => {
66
+ const status = 404;
67
+
68
+ expect(formatToolError(status).content[0].text).toBe(
69
+ "Error: API Response(status: 404)",
70
+ );
71
+ expect(formatResourceError(status, "uri").contents[0].text).toBe(
72
+ "Error: API Response(status: 404)",
73
+ );
74
+ });
75
+
76
+ it("should format object errors as JSON strings", () => {
77
+ const error = { code: "NOT_FOUND", message: "Resource not found" };
78
+ const expectedText = `Error: ${JSON.stringify(error, null, 2)}`;
79
+
80
+ expect(formatToolError(error).content[0].text).toBe(expectedText);
81
+ expect(formatResourceError(error, "uri").contents[0].text).toBe(
82
+ expectedText,
83
+ );
84
+ expect(formatPromptError(error).messages[0].content.text).toBe(
85
+ expectedText,
86
+ );
87
+ });
88
+
89
+ it("should maintain correct structure for each error formatter", () => {
90
+ const error = new Error("test");
91
+ const uri = "test://uri";
92
+
93
+ const toolResult = formatToolError(error);
94
+ expect(toolResult).toHaveProperty("content");
95
+ expect(Array.isArray(toolResult.content)).toBe(true);
96
+
97
+ const resourceResult = formatResourceError(error, uri);
98
+ expect(resourceResult).toHaveProperty("contents");
99
+ expect(resourceResult.contents[0].uri).toBe(uri);
100
+ expect(resourceResult.contents[0].mimeType).toBe("application/json");
101
+
102
+ const promptResult = formatPromptError(error);
103
+ expect(promptResult).toHaveProperty("messages");
104
+ expect(promptResult.messages[0].role).toBe("user");
105
+ });
106
+ });