@esaio/esa-mcp-server 0.2.1 → 0.2.3
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.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/bin/index.js +1 -1
- package/package.json +20 -2
- package/.dockerignore +0 -36
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/docker-publish.yml +0 -120
- package/.github/workflows/main.yml +0 -41
- package/CLAUDE.md +0 -94
- package/Dockerfile +0 -34
- package/biome.json +0 -57
- package/src/__tests__/fixtures/mock-comment.ts +0 -90
- package/src/__tests__/fixtures/mock-post.ts +0 -79
- package/src/__tests__/index.test.ts +0 -216
- package/src/api_client/__tests__/index.test.ts +0 -149
- package/src/api_client/__tests__/middleware.test.ts +0 -120
- package/src/api_client/__tests__/with-context.test.ts +0 -98
- package/src/api_client/index.ts +0 -29
- package/src/api_client/middleware.ts +0 -21
- package/src/api_client/with-context.ts +0 -26
- package/src/config/__tests__/index.test.ts +0 -65
- package/src/config/index.ts +0 -20
- package/src/context/mcp-context.ts +0 -1
- package/src/context/stdio-context.ts +0 -6
- package/src/errors/missing-team-name-error.ts +0 -8
- package/src/formatters/__tests__/mcp-response.test.ts +0 -106
- package/src/formatters/mcp-response.ts +0 -95
- package/src/generated/api-types.ts +0 -2968
- package/src/i18n/__tests__/index.test.ts +0 -53
- package/src/i18n/index.ts +0 -39
- package/src/index.ts +0 -47
- package/src/locales/en.json +0 -13
- package/src/locales/ja.json +0 -13
- package/src/prompts/__tests__/index.test.ts +0 -48
- package/src/prompts/__tests__/summarize-post.test.ts +0 -291
- package/src/prompts/index.ts +0 -21
- package/src/prompts/summarize-post.ts +0 -94
- package/src/resources/__tests__/index.test.ts +0 -50
- package/src/resources/__tests__/recent-posts-list.test.ts +0 -92
- package/src/resources/__tests__/recent-posts.test.ts +0 -270
- package/src/resources/index.ts +0 -33
- package/src/resources/recent-posts-list.ts +0 -22
- package/src/resources/recent-posts.ts +0 -45
- package/src/schemas/team-name-schema.ts +0 -19
- package/src/tools/__tests__/attachments.test.ts +0 -460
- package/src/tools/__tests__/categories.test.ts +0 -402
- package/src/tools/__tests__/comments.test.ts +0 -970
- package/src/tools/__tests__/helps.test.ts +0 -222
- package/src/tools/__tests__/index.test.ts +0 -48
- package/src/tools/__tests__/post-actions.test.ts +0 -445
- package/src/tools/__tests__/posts.test.ts +0 -917
- package/src/tools/__tests__/search.test.ts +0 -339
- package/src/tools/__tests__/teams.test.ts +0 -615
- package/src/tools/attachments.ts +0 -167
- package/src/tools/categories.ts +0 -153
- package/src/tools/comments.ts +0 -258
- package/src/tools/helps.ts +0 -50
- package/src/tools/index.ts +0 -351
- package/src/tools/post-actions.ts +0 -132
- package/src/tools/posts.ts +0 -179
- package/src/tools/search.ts +0 -98
- package/src/tools/teams.ts +0 -157
- package/src/transformers/__tests__/category-transformer.test.ts +0 -161
- package/src/transformers/__tests__/comment-transformer.test.ts +0 -129
- package/src/transformers/__tests__/post-name-normalizer.test.ts +0 -53
- package/src/transformers/__tests__/post-transformer.test.ts +0 -70
- package/src/transformers/__tests__/query-normalizer.test.ts +0 -98
- package/src/transformers/__tests__/team-name-normalizer.test.ts +0 -21
- package/src/transformers/category-transformer.ts +0 -36
- package/src/transformers/comment-transformer.ts +0 -34
- package/src/transformers/post-name-normalizer.ts +0 -30
- package/src/transformers/post-transformer.ts +0 -38
- package/src/transformers/query-normalizer.ts +0 -36
- package/src/transformers/team-name-normalizer.ts +0 -7
- package/tsconfig.build.json +0 -4
- package/tsconfig.json +0 -30
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -24
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import type { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import type { MockInstance } from "vitest";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
describe("MCP Server", () => {
|
|
7
|
-
let consoleErrorSpy: MockInstance<typeof console.error>;
|
|
8
|
-
let processExitSpy: MockInstance<typeof process.exit>;
|
|
9
|
-
|
|
10
|
-
const createMockConfig = (behavior?: { shouldThrow?: boolean }) => ({
|
|
11
|
-
config: {
|
|
12
|
-
server: {
|
|
13
|
-
name: "test-server",
|
|
14
|
-
version: "1.0.0",
|
|
15
|
-
},
|
|
16
|
-
esa: {
|
|
17
|
-
apiAccessToken: "test-token",
|
|
18
|
-
apiBaseUrl: "https://api.esa.example.com",
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
validateConfig: vi.fn().mockImplementation(() => {
|
|
22
|
-
if (behavior?.shouldThrow) {
|
|
23
|
-
throw new Error("Invalid config");
|
|
24
|
-
}
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const createMockServer = (behavior?: { shouldFailConnect?: boolean }) => {
|
|
29
|
-
const mockConnect = behavior?.shouldFailConnect
|
|
30
|
-
? vi.fn().mockRejectedValue(new Error("Connection failed"))
|
|
31
|
-
: vi.fn().mockResolvedValue(undefined);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
connect: mockConnect,
|
|
35
|
-
} as unknown as McpServer;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const createMockTransport = () =>
|
|
39
|
-
({
|
|
40
|
-
onclose: undefined as (() => void) | undefined,
|
|
41
|
-
onerror: undefined as ((error: Error) => void) | undefined,
|
|
42
|
-
}) as unknown as StdioServerTransport;
|
|
43
|
-
|
|
44
|
-
const setupServerMocks = (
|
|
45
|
-
mockConfig: ReturnType<typeof createMockConfig>,
|
|
46
|
-
mockServer: ReturnType<typeof createMockServer>,
|
|
47
|
-
mockTransport: ReturnType<typeof createMockTransport>,
|
|
48
|
-
setupFunctions?: {
|
|
49
|
-
setupTools?: ReturnType<typeof vi.fn>;
|
|
50
|
-
setupResources?: ReturnType<typeof vi.fn>;
|
|
51
|
-
setupPrompts?: ReturnType<typeof vi.fn>;
|
|
52
|
-
initI18n?: ReturnType<typeof vi.fn>;
|
|
53
|
-
},
|
|
54
|
-
) => {
|
|
55
|
-
// Use function keyword for constructor mocks (required in vitest v4)
|
|
56
|
-
// biome-ignore lint/complexity/useArrowFunction: Constructor mocks in vitest v4 require function keyword, not arrow functions
|
|
57
|
-
const MockMcpServer = vi.fn(function (..._args: unknown[]) {
|
|
58
|
-
return mockServer;
|
|
59
|
-
});
|
|
60
|
-
// biome-ignore lint/complexity/useArrowFunction: Constructor mocks in vitest v4 require function keyword, not arrow functions
|
|
61
|
-
const MockStdioServerTransport = vi.fn(function (..._args: unknown[]) {
|
|
62
|
-
return mockTransport;
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
vi.doMock("../config/index.js", () => mockConfig);
|
|
66
|
-
vi.doMock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
|
67
|
-
McpServer: MockMcpServer,
|
|
68
|
-
}));
|
|
69
|
-
vi.doMock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
|
70
|
-
StdioServerTransport: MockStdioServerTransport,
|
|
71
|
-
}));
|
|
72
|
-
vi.doMock("../tools/index.js", () => ({
|
|
73
|
-
setupTools: setupFunctions?.setupTools ?? vi.fn(),
|
|
74
|
-
}));
|
|
75
|
-
vi.doMock("../resources/index.js", () => ({
|
|
76
|
-
setupResources: setupFunctions?.setupResources ?? vi.fn(),
|
|
77
|
-
}));
|
|
78
|
-
vi.doMock("../prompts/index.js", () => ({
|
|
79
|
-
setupPrompts: setupFunctions?.setupPrompts ?? vi.fn(),
|
|
80
|
-
}));
|
|
81
|
-
vi.doMock("../i18n/index.js", () => ({
|
|
82
|
-
initI18n:
|
|
83
|
-
setupFunctions?.initI18n ?? vi.fn().mockResolvedValue(undefined),
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
return { MockMcpServer, MockStdioServerTransport };
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
vi.resetModules();
|
|
91
|
-
vi.clearAllMocks();
|
|
92
|
-
|
|
93
|
-
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
94
|
-
processExitSpy = vi
|
|
95
|
-
.spyOn(process, "exit")
|
|
96
|
-
.mockImplementation(() => undefined as never);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
afterEach(() => {
|
|
100
|
-
consoleErrorSpy.mockRestore();
|
|
101
|
-
processExitSpy.mockRestore();
|
|
102
|
-
vi.resetModules();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should validate config on startup", async () => {
|
|
106
|
-
const mockConfig = createMockConfig();
|
|
107
|
-
const mockServer = createMockServer();
|
|
108
|
-
const mockTransport = createMockTransport();
|
|
109
|
-
|
|
110
|
-
setupServerMocks(mockConfig, mockServer, mockTransport);
|
|
111
|
-
|
|
112
|
-
await import("../index.js");
|
|
113
|
-
|
|
114
|
-
expect(mockConfig.validateConfig).toHaveBeenCalled();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("should exit with error when config validation fails", async () => {
|
|
118
|
-
const mockConfig = createMockConfig({ shouldThrow: true });
|
|
119
|
-
|
|
120
|
-
vi.doMock("../config/index.js", () => mockConfig);
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
await import("../index.js");
|
|
124
|
-
} catch (_error) {
|
|
125
|
-
// Expected to throw due to config validation
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
129
|
-
"Configuration error:",
|
|
130
|
-
expect.any(Error),
|
|
131
|
-
);
|
|
132
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("should create MCP server and connect", async () => {
|
|
136
|
-
const mockConfig = createMockConfig();
|
|
137
|
-
const mockServer = createMockServer();
|
|
138
|
-
const mockTransport = createMockTransport();
|
|
139
|
-
|
|
140
|
-
// Track setup function calls
|
|
141
|
-
const mockSetupTools = vi.fn();
|
|
142
|
-
const mockSetupResources = vi.fn();
|
|
143
|
-
const mockSetupPrompts = vi.fn();
|
|
144
|
-
|
|
145
|
-
const { MockMcpServer } = setupServerMocks(
|
|
146
|
-
mockConfig,
|
|
147
|
-
mockServer,
|
|
148
|
-
mockTransport,
|
|
149
|
-
{
|
|
150
|
-
setupTools: mockSetupTools,
|
|
151
|
-
setupResources: mockSetupResources,
|
|
152
|
-
setupPrompts: mockSetupPrompts,
|
|
153
|
-
},
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
await import("../index.js");
|
|
157
|
-
|
|
158
|
-
expect(MockMcpServer).toHaveBeenCalledWith({
|
|
159
|
-
name: "test-server",
|
|
160
|
-
version: "1.0.0",
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const expectedEsaConfig = {
|
|
164
|
-
apiAccessToken: "test-token",
|
|
165
|
-
apiBaseUrl: "https://api.esa.example.com",
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
expect(mockSetupTools).toHaveBeenCalledWith(mockServer, expectedEsaConfig);
|
|
169
|
-
expect(mockSetupResources).toHaveBeenCalledWith(
|
|
170
|
-
mockServer,
|
|
171
|
-
expectedEsaConfig,
|
|
172
|
-
);
|
|
173
|
-
expect(mockSetupPrompts).toHaveBeenCalledWith(
|
|
174
|
-
mockServer,
|
|
175
|
-
expectedEsaConfig,
|
|
176
|
-
);
|
|
177
|
-
expect(mockServer.connect).toHaveBeenCalled();
|
|
178
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith("test-server v1.0.0 started");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("should set up transport error handlers", async () => {
|
|
182
|
-
const mockConfig = createMockConfig();
|
|
183
|
-
const mockServer = createMockServer();
|
|
184
|
-
const mockTransport = createMockTransport();
|
|
185
|
-
|
|
186
|
-
setupServerMocks(mockConfig, mockServer, mockTransport);
|
|
187
|
-
|
|
188
|
-
await import("../index.js");
|
|
189
|
-
|
|
190
|
-
expect(mockTransport.onclose).toBeDefined();
|
|
191
|
-
expect(mockTransport.onerror).toBeDefined();
|
|
192
|
-
|
|
193
|
-
mockTransport.onclose?.();
|
|
194
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Transport closed");
|
|
195
|
-
|
|
196
|
-
const testError = new Error("Test error");
|
|
197
|
-
mockTransport.onerror?.(testError);
|
|
198
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Transport error:", testError);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("should handle server startup errors", async () => {
|
|
202
|
-
const mockConfig = createMockConfig();
|
|
203
|
-
const mockServer = createMockServer({ shouldFailConnect: true });
|
|
204
|
-
const mockTransport = createMockTransport();
|
|
205
|
-
|
|
206
|
-
setupServerMocks(mockConfig, mockServer, mockTransport);
|
|
207
|
-
|
|
208
|
-
await import("../index.js");
|
|
209
|
-
|
|
210
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
211
|
-
"Server startup error:",
|
|
212
|
-
expect.any(Error),
|
|
213
|
-
);
|
|
214
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { MockInstance } from "vitest";
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { createAuthMiddleware } from "../middleware.js";
|
|
4
|
-
|
|
5
|
-
describe("createAuthMiddleware", () => {
|
|
6
|
-
// Mock console.error to verify rate limit and error logging without console output
|
|
7
|
-
let consoleErrorSpy: MockInstance<typeof console.error>;
|
|
8
|
-
|
|
9
|
-
// Create test request for authorization header testing
|
|
10
|
-
const createTestRequest = (url = "https://api.esa.example.com/test") =>
|
|
11
|
-
new Request(url);
|
|
12
|
-
|
|
13
|
-
// Create test response with optional rate limit headers
|
|
14
|
-
const createTestResponse = (rateLimitHeaders?: {
|
|
15
|
-
limit?: string;
|
|
16
|
-
remaining?: string;
|
|
17
|
-
}) => {
|
|
18
|
-
const headers: Record<string, string> = {};
|
|
19
|
-
if (rateLimitHeaders?.limit) {
|
|
20
|
-
headers["x-ratelimit-limit"] = rateLimitHeaders.limit;
|
|
21
|
-
}
|
|
22
|
-
if (rateLimitHeaders?.remaining) {
|
|
23
|
-
headers["x-ratelimit-remaining"] = rateLimitHeaders.remaining;
|
|
24
|
-
}
|
|
25
|
-
return new Response(null, { headers });
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Helper to call middleware methods with minimal required context using as unknown as pattern
|
|
29
|
-
const callOnRequest = (
|
|
30
|
-
middleware: ReturnType<typeof createAuthMiddleware>,
|
|
31
|
-
request: Request,
|
|
32
|
-
) =>
|
|
33
|
-
middleware.onRequest?.({
|
|
34
|
-
request,
|
|
35
|
-
schemaPath: "/test",
|
|
36
|
-
params: {},
|
|
37
|
-
id: "test-id",
|
|
38
|
-
options: { baseUrl: "https://api.esa.example.com" },
|
|
39
|
-
} as unknown as Parameters<NonNullable<typeof middleware.onRequest>>[0]);
|
|
40
|
-
|
|
41
|
-
const callOnResponse = (
|
|
42
|
-
middleware: ReturnType<typeof createAuthMiddleware>,
|
|
43
|
-
response: Response,
|
|
44
|
-
) =>
|
|
45
|
-
middleware.onResponse?.({
|
|
46
|
-
request: createTestRequest(),
|
|
47
|
-
response,
|
|
48
|
-
schemaPath: "/test",
|
|
49
|
-
params: {},
|
|
50
|
-
id: "test-id",
|
|
51
|
-
options: { baseUrl: "https://api.esa.example.com" },
|
|
52
|
-
} as unknown as Parameters<NonNullable<typeof middleware.onResponse>>[0]);
|
|
53
|
-
|
|
54
|
-
const callOnError = (
|
|
55
|
-
middleware: ReturnType<typeof createAuthMiddleware>,
|
|
56
|
-
error: Error,
|
|
57
|
-
) =>
|
|
58
|
-
middleware.onError?.({
|
|
59
|
-
request: createTestRequest(),
|
|
60
|
-
error,
|
|
61
|
-
schemaPath: "/test",
|
|
62
|
-
params: {},
|
|
63
|
-
id: "test-id",
|
|
64
|
-
options: { baseUrl: "https://api.esa.example.com" },
|
|
65
|
-
} as unknown as Parameters<NonNullable<typeof middleware.onError>>[0]);
|
|
66
|
-
|
|
67
|
-
beforeEach(() => {
|
|
68
|
-
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
afterEach(() => {
|
|
72
|
-
consoleErrorSpy.mockRestore();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("should add authorization header", async () => {
|
|
76
|
-
const middleware = createAuthMiddleware("test-token");
|
|
77
|
-
const request = createTestRequest();
|
|
78
|
-
|
|
79
|
-
expect(middleware.onRequest).toBeDefined();
|
|
80
|
-
expect(request.headers.get("Authorization")).toBeNull();
|
|
81
|
-
|
|
82
|
-
await callOnRequest(middleware, request);
|
|
83
|
-
|
|
84
|
-
expect(request.headers.get("Authorization")).toBe("Bearer test-token");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should log rate limit info when present", async () => {
|
|
88
|
-
const middleware = createAuthMiddleware("test-token");
|
|
89
|
-
const response = createTestResponse({ limit: "75", remaining: "50" });
|
|
90
|
-
|
|
91
|
-
await callOnResponse(middleware, response);
|
|
92
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Rate limit: 50/75");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("should handle response without rate limit headers", async () => {
|
|
96
|
-
const middleware = createAuthMiddleware("test-token");
|
|
97
|
-
const response = createTestResponse();
|
|
98
|
-
|
|
99
|
-
await callOnResponse(middleware, response);
|
|
100
|
-
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("should log network errors", async () => {
|
|
104
|
-
const middleware = createAuthMiddleware("test-token");
|
|
105
|
-
const error = new Error("Network failed");
|
|
106
|
-
|
|
107
|
-
await callOnError(middleware, error);
|
|
108
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith("Network Error:", error);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("should return the modified request", async () => {
|
|
112
|
-
const middleware = createAuthMiddleware("test-token");
|
|
113
|
-
const request = createTestRequest();
|
|
114
|
-
|
|
115
|
-
const result = await callOnRequest(middleware, request);
|
|
116
|
-
|
|
117
|
-
expect(result).toBe(request);
|
|
118
|
-
expect(request.headers.get("Authorization")).toBe("Bearer test-token");
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
});
|
package/src/api_client/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
}
|