@access-mcp/shared 0.3.3 → 0.6.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.
- package/dist/__tests__/base-server.test.d.ts +1 -0
- package/dist/__tests__/base-server.test.js +176 -0
- package/dist/__tests__/interserver.integration.test.d.ts +1 -0
- package/dist/__tests__/interserver.integration.test.js +118 -0
- package/dist/__tests__/utils.test.js +3 -3
- package/dist/base-server.d.ts +19 -16
- package/dist/base-server.js +146 -26
- package/dist/drupal-auth.d.ts +60 -0
- package/dist/drupal-auth.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +42 -0
- package/dist/logger.js +82 -0
- package/dist/taxonomies.js +19 -90
- package/dist/utils.d.ts +4 -4
- package/dist/utils.js +9 -5
- package/package.json +1 -1
package/dist/base-server.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3
4
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
5
|
import axios from "axios";
|
|
5
6
|
import express from "express";
|
|
7
|
+
import { createLogger } from "./logger.js";
|
|
6
8
|
export class BaseAccessServer {
|
|
7
9
|
serverName;
|
|
8
10
|
version;
|
|
9
11
|
baseURL;
|
|
10
12
|
server;
|
|
11
13
|
transport;
|
|
14
|
+
logger;
|
|
12
15
|
_httpClient;
|
|
13
16
|
_httpServer;
|
|
14
17
|
_httpPort;
|
|
18
|
+
_sseTransports = new Map();
|
|
15
19
|
constructor(serverName, version, baseURL = "https://support.access-ci.org/api") {
|
|
16
20
|
this.serverName = serverName;
|
|
17
21
|
this.version = version;
|
|
18
22
|
this.baseURL = baseURL;
|
|
23
|
+
this.logger = createLogger(serverName);
|
|
19
24
|
this.server = new Server({
|
|
20
25
|
name: serverName,
|
|
21
26
|
version: version,
|
|
@@ -73,13 +78,13 @@ export class BaseAccessServer {
|
|
|
73
78
|
}
|
|
74
79
|
catch (error) {
|
|
75
80
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
76
|
-
|
|
81
|
+
this.logger.error("Error handling tool call", { error: errorMessage });
|
|
77
82
|
return {
|
|
78
83
|
content: [
|
|
79
84
|
{
|
|
80
85
|
type: "text",
|
|
81
86
|
text: JSON.stringify({
|
|
82
|
-
error: errorMessage
|
|
87
|
+
error: errorMessage,
|
|
83
88
|
}),
|
|
84
89
|
},
|
|
85
90
|
],
|
|
@@ -93,7 +98,7 @@ export class BaseAccessServer {
|
|
|
93
98
|
}
|
|
94
99
|
catch (error) {
|
|
95
100
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
96
|
-
|
|
101
|
+
this.logger.error("Error reading resource", { error: errorMessage });
|
|
97
102
|
return {
|
|
98
103
|
contents: [
|
|
99
104
|
{
|
|
@@ -120,7 +125,7 @@ export class BaseAccessServer {
|
|
|
120
125
|
}
|
|
121
126
|
catch (error) {
|
|
122
127
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
123
|
-
|
|
128
|
+
this.logger.error("Error getting prompt", { error: errorMessage });
|
|
124
129
|
throw error;
|
|
125
130
|
}
|
|
126
131
|
});
|
|
@@ -135,13 +140,13 @@ export class BaseAccessServer {
|
|
|
135
140
|
* Handle resource read requests - override in subclasses
|
|
136
141
|
*/
|
|
137
142
|
async handleResourceRead(request) {
|
|
138
|
-
throw new Error(
|
|
143
|
+
throw new Error(`Resource reading not supported by this server: ${request.params.uri}`);
|
|
139
144
|
}
|
|
140
145
|
/**
|
|
141
146
|
* Handle get prompt requests - override in subclasses
|
|
142
147
|
*/
|
|
143
148
|
async handleGetPrompt(request) {
|
|
144
|
-
throw new Error(
|
|
149
|
+
throw new Error(`Prompt not found: ${request.params.name}`);
|
|
145
150
|
}
|
|
146
151
|
/**
|
|
147
152
|
* Helper method to create a standard error response (MCP 2025 compliant)
|
|
@@ -218,7 +223,7 @@ export class BaseAccessServer {
|
|
|
218
223
|
if (options?.httpPort) {
|
|
219
224
|
this._httpPort = options.httpPort;
|
|
220
225
|
await this.startHttpService();
|
|
221
|
-
|
|
226
|
+
this.logger.info("HTTP server running", { port: this._httpPort });
|
|
222
227
|
}
|
|
223
228
|
else {
|
|
224
229
|
// Only connect stdio transport when NOT in HTTP mode
|
|
@@ -228,7 +233,7 @@ export class BaseAccessServer {
|
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
235
|
/**
|
|
231
|
-
* Start HTTP service layer for
|
|
236
|
+
* Start HTTP service layer with SSE support for remote MCP connections
|
|
232
237
|
*/
|
|
233
238
|
async startHttpService() {
|
|
234
239
|
if (!this._httpPort)
|
|
@@ -236,41 +241,79 @@ export class BaseAccessServer {
|
|
|
236
241
|
this._httpServer = express();
|
|
237
242
|
this._httpServer.use(express.json());
|
|
238
243
|
// Health check endpoint
|
|
239
|
-
this._httpServer.get(
|
|
244
|
+
this._httpServer.get("/health", (req, res) => {
|
|
240
245
|
res.json({
|
|
241
246
|
server: this.serverName,
|
|
242
247
|
version: this.version,
|
|
243
|
-
status:
|
|
244
|
-
timestamp: new Date().toISOString()
|
|
248
|
+
status: "healthy",
|
|
249
|
+
timestamp: new Date().toISOString(),
|
|
245
250
|
});
|
|
246
251
|
});
|
|
247
|
-
//
|
|
248
|
-
this._httpServer.get(
|
|
252
|
+
// SSE endpoint for MCP remote connections
|
|
253
|
+
this._httpServer.get("/sse", async (req, res) => {
|
|
254
|
+
this.logger.debug("New SSE connection");
|
|
255
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
256
|
+
const sessionId = transport.sessionId;
|
|
257
|
+
this._sseTransports.set(sessionId, transport);
|
|
258
|
+
// Clean up on disconnect
|
|
259
|
+
res.on("close", () => {
|
|
260
|
+
this.logger.debug("SSE connection closed", { sessionId });
|
|
261
|
+
this._sseTransports.delete(sessionId);
|
|
262
|
+
});
|
|
263
|
+
// Create a new server instance for this SSE connection
|
|
264
|
+
const sseServer = new Server({
|
|
265
|
+
name: this.serverName,
|
|
266
|
+
version: this.version,
|
|
267
|
+
}, {
|
|
268
|
+
capabilities: {
|
|
269
|
+
resources: {},
|
|
270
|
+
tools: {},
|
|
271
|
+
prompts: {},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
// Set up handlers for the SSE server (same as main server)
|
|
275
|
+
this.setupServerHandlers(sseServer);
|
|
276
|
+
await sseServer.connect(transport);
|
|
277
|
+
});
|
|
278
|
+
// Messages endpoint for SSE POST messages
|
|
279
|
+
this._httpServer.post("/messages", async (req, res) => {
|
|
280
|
+
const sessionId = req.query.sessionId;
|
|
281
|
+
const transport = this._sseTransports.get(sessionId);
|
|
282
|
+
if (!transport) {
|
|
283
|
+
res.status(404).json({ error: "Session not found" });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
287
|
+
});
|
|
288
|
+
// List available tools endpoint (for inter-server communication)
|
|
289
|
+
this._httpServer.get("/tools", (req, res) => {
|
|
249
290
|
try {
|
|
250
291
|
const tools = this.getTools();
|
|
251
292
|
res.json({ tools });
|
|
252
293
|
}
|
|
253
294
|
catch (error) {
|
|
254
|
-
res.status(500).json({ error:
|
|
295
|
+
res.status(500).json({ error: "Failed to list tools" });
|
|
255
296
|
}
|
|
256
297
|
});
|
|
257
|
-
// Tool execution endpoint
|
|
258
|
-
this._httpServer.post(
|
|
298
|
+
// Tool execution endpoint (for inter-server communication)
|
|
299
|
+
this._httpServer.post("/tools/:toolName", async (req, res) => {
|
|
259
300
|
try {
|
|
260
301
|
const { toolName } = req.params;
|
|
261
302
|
const { arguments: args = {} } = req.body;
|
|
262
303
|
// Validate that the tool exists
|
|
263
304
|
const tools = this.getTools();
|
|
264
|
-
const tool = tools.find(t => t.name === toolName);
|
|
305
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
265
306
|
if (!tool) {
|
|
266
|
-
|
|
307
|
+
res.status(404).json({ error: `Tool '${toolName}' not found` });
|
|
308
|
+
return;
|
|
267
309
|
}
|
|
268
310
|
// Execute the tool
|
|
269
311
|
const request = {
|
|
312
|
+
method: "tools/call",
|
|
270
313
|
params: {
|
|
271
314
|
name: toolName,
|
|
272
|
-
arguments: args
|
|
273
|
-
}
|
|
315
|
+
arguments: args,
|
|
316
|
+
},
|
|
274
317
|
};
|
|
275
318
|
const result = await this.handleToolCall(request);
|
|
276
319
|
res.json(result);
|
|
@@ -282,9 +325,86 @@ export class BaseAccessServer {
|
|
|
282
325
|
});
|
|
283
326
|
// Start HTTP server
|
|
284
327
|
return new Promise((resolve, reject) => {
|
|
285
|
-
this._httpServer.listen(this._httpPort,
|
|
328
|
+
this._httpServer.listen(this._httpPort, "0.0.0.0", () => {
|
|
286
329
|
resolve();
|
|
287
|
-
}).on(
|
|
330
|
+
}).on("error", reject);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Set up MCP handlers on a server instance
|
|
335
|
+
*/
|
|
336
|
+
setupServerHandlers(server) {
|
|
337
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
338
|
+
try {
|
|
339
|
+
return { tools: this.getTools() };
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
return { tools: [] };
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
346
|
+
try {
|
|
347
|
+
return { resources: this.getResources() };
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
return { resources: [] };
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
354
|
+
try {
|
|
355
|
+
return await this.handleToolCall(request);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
359
|
+
this.logger.error("Error handling tool call", { error: errorMessage });
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: JSON.stringify({
|
|
365
|
+
error: errorMessage,
|
|
366
|
+
}),
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
isError: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
374
|
+
try {
|
|
375
|
+
return await this.handleResourceRead(request);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
379
|
+
this.logger.error("Error reading resource", { error: errorMessage });
|
|
380
|
+
return {
|
|
381
|
+
contents: [
|
|
382
|
+
{
|
|
383
|
+
uri: request.params.uri,
|
|
384
|
+
mimeType: "text/plain",
|
|
385
|
+
text: `Error: ${errorMessage}`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
392
|
+
try {
|
|
393
|
+
return { prompts: this.getPrompts() };
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
return { prompts: [] };
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
400
|
+
try {
|
|
401
|
+
return await this.handleGetPrompt(request);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
405
|
+
this.logger.error("Error getting prompt", { error: errorMessage });
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
288
408
|
});
|
|
289
409
|
}
|
|
290
410
|
/**
|
|
@@ -296,10 +416,10 @@ export class BaseAccessServer {
|
|
|
296
416
|
throw new Error(`Service '${serviceName}' not found. Check ACCESS_MCP_SERVICES environment variable.`);
|
|
297
417
|
}
|
|
298
418
|
const response = await axios.post(`${serviceUrl}/tools/${toolName}`, {
|
|
299
|
-
arguments: args
|
|
419
|
+
arguments: args,
|
|
300
420
|
}, {
|
|
301
421
|
timeout: 30000,
|
|
302
|
-
validateStatus: () => true
|
|
422
|
+
validateStatus: () => true,
|
|
303
423
|
});
|
|
304
424
|
if (response.status !== 200) {
|
|
305
425
|
throw new Error(`Remote server call failed: ${response.status} ${response.data?.error || response.statusText}`);
|
|
@@ -315,8 +435,8 @@ export class BaseAccessServer {
|
|
|
315
435
|
if (!services)
|
|
316
436
|
return null;
|
|
317
437
|
const serviceMap = {};
|
|
318
|
-
services.split(
|
|
319
|
-
const [name, url] = service.split(
|
|
438
|
+
services.split(",").forEach((service) => {
|
|
439
|
+
const [name, url] = service.split("=");
|
|
320
440
|
if (name && url) {
|
|
321
441
|
serviceMap[name.trim()] = url.trim();
|
|
322
442
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication provider for Drupal JSON:API using cookie-based auth.
|
|
3
|
+
*
|
|
4
|
+
* This is a temporary implementation for development/testing.
|
|
5
|
+
* Production should use Key Auth with the access_mcp_author module.
|
|
6
|
+
*
|
|
7
|
+
* @see ../../../access-qa-planning/06-mcp-authentication.md
|
|
8
|
+
*/
|
|
9
|
+
export declare class DrupalAuthProvider {
|
|
10
|
+
private baseUrl;
|
|
11
|
+
private username;
|
|
12
|
+
private password;
|
|
13
|
+
private sessionCookie?;
|
|
14
|
+
private csrfToken?;
|
|
15
|
+
private logoutToken?;
|
|
16
|
+
private userUuid?;
|
|
17
|
+
private httpClient;
|
|
18
|
+
private isAuthenticated;
|
|
19
|
+
constructor(baseUrl: string, username: string, password: string);
|
|
20
|
+
/**
|
|
21
|
+
* Ensure we have a valid session, logging in if necessary
|
|
22
|
+
*/
|
|
23
|
+
ensureAuthenticated(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Login to Drupal and store session cookie + CSRF token
|
|
26
|
+
*/
|
|
27
|
+
login(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Get headers required for authenticated JSON:API requests
|
|
30
|
+
*/
|
|
31
|
+
getAuthHeaders(): Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Get the authenticated user's UUID
|
|
34
|
+
*/
|
|
35
|
+
getUserUuid(): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Invalidate the current session
|
|
38
|
+
*/
|
|
39
|
+
invalidate(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Make an authenticated GET request to JSON:API
|
|
42
|
+
*/
|
|
43
|
+
get(path: string): Promise<any>;
|
|
44
|
+
/**
|
|
45
|
+
* Make an authenticated POST request to JSON:API
|
|
46
|
+
*/
|
|
47
|
+
post(path: string, data: any): Promise<any>;
|
|
48
|
+
/**
|
|
49
|
+
* Make an authenticated PATCH request to JSON:API
|
|
50
|
+
*/
|
|
51
|
+
patch(path: string, data: any): Promise<any>;
|
|
52
|
+
/**
|
|
53
|
+
* Make an authenticated DELETE request to JSON:API
|
|
54
|
+
*/
|
|
55
|
+
delete(path: string): Promise<any>;
|
|
56
|
+
/**
|
|
57
|
+
* Handle JSON:API response, throwing on errors
|
|
58
|
+
*/
|
|
59
|
+
private handleResponse;
|
|
60
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
/**
|
|
3
|
+
* Authentication provider for Drupal JSON:API using cookie-based auth.
|
|
4
|
+
*
|
|
5
|
+
* This is a temporary implementation for development/testing.
|
|
6
|
+
* Production should use Key Auth with the access_mcp_author module.
|
|
7
|
+
*
|
|
8
|
+
* @see ../../../access-qa-planning/06-mcp-authentication.md
|
|
9
|
+
*/
|
|
10
|
+
export class DrupalAuthProvider {
|
|
11
|
+
baseUrl;
|
|
12
|
+
username;
|
|
13
|
+
password;
|
|
14
|
+
sessionCookie;
|
|
15
|
+
csrfToken;
|
|
16
|
+
logoutToken;
|
|
17
|
+
userUuid;
|
|
18
|
+
httpClient;
|
|
19
|
+
isAuthenticated = false;
|
|
20
|
+
constructor(baseUrl, username, password) {
|
|
21
|
+
this.baseUrl = baseUrl;
|
|
22
|
+
this.username = username;
|
|
23
|
+
this.password = password;
|
|
24
|
+
this.httpClient = axios.create({
|
|
25
|
+
baseURL: this.baseUrl,
|
|
26
|
+
timeout: 30000,
|
|
27
|
+
validateStatus: () => true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ensure we have a valid session, logging in if necessary
|
|
32
|
+
*/
|
|
33
|
+
async ensureAuthenticated() {
|
|
34
|
+
if (!this.isAuthenticated) {
|
|
35
|
+
await this.login();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Login to Drupal and store session cookie + CSRF token
|
|
40
|
+
*/
|
|
41
|
+
async login() {
|
|
42
|
+
const response = await this.httpClient.post("/user/login?_format=json", {
|
|
43
|
+
name: this.username,
|
|
44
|
+
pass: this.password,
|
|
45
|
+
}, {
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (response.status !== 200) {
|
|
51
|
+
throw new Error(`Drupal login failed: ${response.status} ${response.statusText}`);
|
|
52
|
+
}
|
|
53
|
+
// Extract session cookie from Set-Cookie header
|
|
54
|
+
const setCookie = response.headers["set-cookie"];
|
|
55
|
+
if (setCookie && setCookie.length > 0) {
|
|
56
|
+
// Parse the session cookie (format: SESS...=value; path=/; ...)
|
|
57
|
+
const cookieParts = setCookie[0].split(";")[0];
|
|
58
|
+
this.sessionCookie = cookieParts;
|
|
59
|
+
}
|
|
60
|
+
// Store CSRF token and logout token from response
|
|
61
|
+
this.csrfToken = response.data.csrf_token;
|
|
62
|
+
this.logoutToken = response.data.logout_token;
|
|
63
|
+
this.userUuid = response.data.current_user?.uuid;
|
|
64
|
+
if (!this.sessionCookie || !this.csrfToken) {
|
|
65
|
+
throw new Error("Login succeeded but missing session cookie or CSRF token");
|
|
66
|
+
}
|
|
67
|
+
this.isAuthenticated = true;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get headers required for authenticated JSON:API requests
|
|
71
|
+
*/
|
|
72
|
+
getAuthHeaders() {
|
|
73
|
+
if (!this.isAuthenticated || !this.sessionCookie || !this.csrfToken) {
|
|
74
|
+
throw new Error("Not authenticated. Call ensureAuthenticated() first.");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
Cookie: this.sessionCookie,
|
|
78
|
+
"X-CSRF-Token": this.csrfToken,
|
|
79
|
+
"Content-Type": "application/vnd.api+json",
|
|
80
|
+
Accept: "application/vnd.api+json",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get the authenticated user's UUID
|
|
85
|
+
*/
|
|
86
|
+
getUserUuid() {
|
|
87
|
+
return this.userUuid;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Invalidate the current session
|
|
91
|
+
*/
|
|
92
|
+
invalidate() {
|
|
93
|
+
this.sessionCookie = undefined;
|
|
94
|
+
this.csrfToken = undefined;
|
|
95
|
+
this.logoutToken = undefined;
|
|
96
|
+
this.userUuid = undefined;
|
|
97
|
+
this.isAuthenticated = false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Make an authenticated GET request to JSON:API
|
|
101
|
+
*/
|
|
102
|
+
async get(path) {
|
|
103
|
+
await this.ensureAuthenticated();
|
|
104
|
+
const response = await this.httpClient.get(path, {
|
|
105
|
+
headers: this.getAuthHeaders(),
|
|
106
|
+
});
|
|
107
|
+
if (response.status === 401 || response.status === 403) {
|
|
108
|
+
// Session may have expired, try re-authenticating
|
|
109
|
+
this.invalidate();
|
|
110
|
+
await this.ensureAuthenticated();
|
|
111
|
+
const retryResponse = await this.httpClient.get(path, {
|
|
112
|
+
headers: this.getAuthHeaders(),
|
|
113
|
+
});
|
|
114
|
+
return this.handleResponse(retryResponse);
|
|
115
|
+
}
|
|
116
|
+
return this.handleResponse(response);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Make an authenticated POST request to JSON:API
|
|
120
|
+
*/
|
|
121
|
+
async post(path, data) {
|
|
122
|
+
await this.ensureAuthenticated();
|
|
123
|
+
const response = await this.httpClient.post(path, data, {
|
|
124
|
+
headers: this.getAuthHeaders(),
|
|
125
|
+
});
|
|
126
|
+
if (response.status === 401 || response.status === 403) {
|
|
127
|
+
this.invalidate();
|
|
128
|
+
await this.ensureAuthenticated();
|
|
129
|
+
const retryResponse = await this.httpClient.post(path, data, {
|
|
130
|
+
headers: this.getAuthHeaders(),
|
|
131
|
+
});
|
|
132
|
+
return this.handleResponse(retryResponse);
|
|
133
|
+
}
|
|
134
|
+
return this.handleResponse(response);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Make an authenticated PATCH request to JSON:API
|
|
138
|
+
*/
|
|
139
|
+
async patch(path, data) {
|
|
140
|
+
await this.ensureAuthenticated();
|
|
141
|
+
const response = await this.httpClient.patch(path, data, {
|
|
142
|
+
headers: this.getAuthHeaders(),
|
|
143
|
+
});
|
|
144
|
+
if (response.status === 401 || response.status === 403) {
|
|
145
|
+
this.invalidate();
|
|
146
|
+
await this.ensureAuthenticated();
|
|
147
|
+
const retryResponse = await this.httpClient.patch(path, data, {
|
|
148
|
+
headers: this.getAuthHeaders(),
|
|
149
|
+
});
|
|
150
|
+
return this.handleResponse(retryResponse);
|
|
151
|
+
}
|
|
152
|
+
return this.handleResponse(response);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Make an authenticated DELETE request to JSON:API
|
|
156
|
+
*/
|
|
157
|
+
async delete(path) {
|
|
158
|
+
await this.ensureAuthenticated();
|
|
159
|
+
const response = await this.httpClient.delete(path, {
|
|
160
|
+
headers: this.getAuthHeaders(),
|
|
161
|
+
});
|
|
162
|
+
if (response.status === 401 || response.status === 403) {
|
|
163
|
+
this.invalidate();
|
|
164
|
+
await this.ensureAuthenticated();
|
|
165
|
+
const retryResponse = await this.httpClient.delete(path, {
|
|
166
|
+
headers: this.getAuthHeaders(),
|
|
167
|
+
});
|
|
168
|
+
return this.handleResponse(retryResponse);
|
|
169
|
+
}
|
|
170
|
+
return this.handleResponse(response);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Handle JSON:API response, throwing on errors
|
|
174
|
+
*/
|
|
175
|
+
handleResponse(response) {
|
|
176
|
+
if (response.status >= 200 && response.status < 300) {
|
|
177
|
+
return response.data;
|
|
178
|
+
}
|
|
179
|
+
// JSON:API error format
|
|
180
|
+
if (response.data?.errors) {
|
|
181
|
+
const errors = response.data.errors
|
|
182
|
+
.map((e) => e.detail || e.title || "Unknown error")
|
|
183
|
+
.join("; ");
|
|
184
|
+
throw new Error(`Drupal API error (${response.status}): ${errors}`);
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Drupal API error: ${response.status} ${response.statusText}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logger for ACCESS-CI MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* This logger writes to stderr to avoid interfering with MCP's JSON-RPC
|
|
5
|
+
* communication on stdout. It supports log levels that can be controlled
|
|
6
|
+
* via the LOG_LEVEL environment variable.
|
|
7
|
+
*
|
|
8
|
+
* Log levels (in order of severity):
|
|
9
|
+
* - error: Always shown, for critical errors
|
|
10
|
+
* - warn: Warnings that don't prevent operation
|
|
11
|
+
* - info: Important informational messages
|
|
12
|
+
* - debug: Detailed debugging information (disabled by default)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { createLogger } from "@access-mcp/shared";
|
|
16
|
+
* const logger = createLogger("my-server");
|
|
17
|
+
* logger.info("Server started");
|
|
18
|
+
* logger.error("Failed to connect", { url: "http://..." });
|
|
19
|
+
*/
|
|
20
|
+
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
21
|
+
interface LogContext {
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface Logger {
|
|
25
|
+
error: (message: string, context?: LogContext) => void;
|
|
26
|
+
warn: (message: string, context?: LogContext) => void;
|
|
27
|
+
info: (message: string, context?: LogContext) => void;
|
|
28
|
+
debug: (message: string, context?: LogContext) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a logger instance for a specific server.
|
|
32
|
+
*
|
|
33
|
+
* @param serverName - The name of the server (e.g., "access-mcp-events")
|
|
34
|
+
* @returns A logger instance with error, warn, info, and debug methods
|
|
35
|
+
*/
|
|
36
|
+
export declare function createLogger(serverName: string): Logger;
|
|
37
|
+
/**
|
|
38
|
+
* A no-op logger that silently discards all log messages.
|
|
39
|
+
* Useful for testing or when logging should be completely disabled.
|
|
40
|
+
*/
|
|
41
|
+
export declare const silentLogger: Logger;
|
|
42
|
+
export {};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logger for ACCESS-CI MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* This logger writes to stderr to avoid interfering with MCP's JSON-RPC
|
|
5
|
+
* communication on stdout. It supports log levels that can be controlled
|
|
6
|
+
* via the LOG_LEVEL environment variable.
|
|
7
|
+
*
|
|
8
|
+
* Log levels (in order of severity):
|
|
9
|
+
* - error: Always shown, for critical errors
|
|
10
|
+
* - warn: Warnings that don't prevent operation
|
|
11
|
+
* - info: Important informational messages
|
|
12
|
+
* - debug: Detailed debugging information (disabled by default)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { createLogger } from "@access-mcp/shared";
|
|
16
|
+
* const logger = createLogger("my-server");
|
|
17
|
+
* logger.info("Server started");
|
|
18
|
+
* logger.error("Failed to connect", { url: "http://..." });
|
|
19
|
+
*/
|
|
20
|
+
const LOG_LEVELS = {
|
|
21
|
+
error: 0,
|
|
22
|
+
warn: 1,
|
|
23
|
+
info: 2,
|
|
24
|
+
debug: 3,
|
|
25
|
+
};
|
|
26
|
+
function getLogLevel() {
|
|
27
|
+
const level = process.env.LOG_LEVEL?.toLowerCase();
|
|
28
|
+
if (level && level in LOG_LEVELS) {
|
|
29
|
+
return level;
|
|
30
|
+
}
|
|
31
|
+
// Default to 'warn' for production (errors and warnings only)
|
|
32
|
+
return "warn";
|
|
33
|
+
}
|
|
34
|
+
function shouldLog(level) {
|
|
35
|
+
const currentLevel = getLogLevel();
|
|
36
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[currentLevel];
|
|
37
|
+
}
|
|
38
|
+
function formatMessage(serverName, level, message, context) {
|
|
39
|
+
const timestamp = new Date().toISOString();
|
|
40
|
+
const contextStr = context ? ` ${JSON.stringify(context)}` : "";
|
|
41
|
+
return `[${timestamp}] [${serverName}] [${level.toUpperCase()}] ${message}${contextStr}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a logger instance for a specific server.
|
|
45
|
+
*
|
|
46
|
+
* @param serverName - The name of the server (e.g., "access-mcp-events")
|
|
47
|
+
* @returns A logger instance with error, warn, info, and debug methods
|
|
48
|
+
*/
|
|
49
|
+
export function createLogger(serverName) {
|
|
50
|
+
return {
|
|
51
|
+
error: (message, context) => {
|
|
52
|
+
if (shouldLog("error")) {
|
|
53
|
+
console.error(formatMessage(serverName, "error", message, context));
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
warn: (message, context) => {
|
|
57
|
+
if (shouldLog("warn")) {
|
|
58
|
+
console.error(formatMessage(serverName, "warn", message, context));
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
info: (message, context) => {
|
|
62
|
+
if (shouldLog("info")) {
|
|
63
|
+
console.error(formatMessage(serverName, "info", message, context));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
debug: (message, context) => {
|
|
67
|
+
if (shouldLog("debug")) {
|
|
68
|
+
console.error(formatMessage(serverName, "debug", message, context));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A no-op logger that silently discards all log messages.
|
|
75
|
+
* Useful for testing or when logging should be completely disabled.
|
|
76
|
+
*/
|
|
77
|
+
export const silentLogger = {
|
|
78
|
+
error: () => { },
|
|
79
|
+
warn: () => { },
|
|
80
|
+
info: () => { },
|
|
81
|
+
debug: () => { },
|
|
82
|
+
};
|