@access-mcp/shared 0.3.2 → 0.5.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 +116 -0
- package/dist/__tests__/utils.test.js +5 -5
- package/dist/base-server.d.ts +22 -9
- package/dist/base-server.js +146 -7
- package/dist/drupal-auth.d.ts +60 -0
- package/dist/drupal-auth.js +188 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +25 -58
- package/dist/types.js +1 -21
- package/dist/utils.d.ts +72 -1
- package/dist/utils.js +74 -5
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { BaseAccessServer } from "../base-server.js";
|
|
3
|
+
// Concrete implementation for testing
|
|
4
|
+
class TestServer extends BaseAccessServer {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("test-server", "1.0.0", "https://api.example.com");
|
|
7
|
+
}
|
|
8
|
+
getTools() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
name: "test_tool",
|
|
12
|
+
description: "A test tool",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
message: { type: "string" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
getResources() {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
uri: "test://resource",
|
|
26
|
+
name: "Test Resource",
|
|
27
|
+
mimeType: "application/json",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
async handleToolCall(request) {
|
|
32
|
+
if (request.params.name === "test_tool") {
|
|
33
|
+
const args = request.params.arguments;
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: JSON.stringify({ echo: args.message || "no message" }),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return this.errorResponse(`Unknown tool: ${request.params.name}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
describe("BaseAccessServer HTTP Mode", () => {
|
|
47
|
+
let server;
|
|
48
|
+
let port;
|
|
49
|
+
let baseUrl;
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
server = new TestServer();
|
|
52
|
+
port = 3100 + Math.floor(Math.random() * 100);
|
|
53
|
+
baseUrl = `http://localhost:${port}`;
|
|
54
|
+
await server.start({ httpPort: port });
|
|
55
|
+
});
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
// Server cleanup handled by process exit
|
|
58
|
+
});
|
|
59
|
+
describe("Health endpoint", () => {
|
|
60
|
+
it("should return health status", async () => {
|
|
61
|
+
const response = await fetch(`${baseUrl}/health`);
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
expect(response.status).toBe(200);
|
|
64
|
+
expect(data.server).toBe("test-server");
|
|
65
|
+
expect(data.version).toBe("1.0.0");
|
|
66
|
+
expect(data.status).toBe("healthy");
|
|
67
|
+
expect(data.timestamp).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("Tools endpoint", () => {
|
|
71
|
+
it("should list available tools", async () => {
|
|
72
|
+
const response = await fetch(`${baseUrl}/tools`);
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
expect(response.status).toBe(200);
|
|
75
|
+
expect(data.tools).toHaveLength(1);
|
|
76
|
+
expect(data.tools[0].name).toBe("test_tool");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("Tool execution endpoint", () => {
|
|
80
|
+
it("should execute a tool via POST", async () => {
|
|
81
|
+
const response = await fetch(`${baseUrl}/tools/test_tool`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ arguments: { message: "hello" } }),
|
|
85
|
+
});
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
expect(response.status).toBe(200);
|
|
88
|
+
expect(data.content).toBeDefined();
|
|
89
|
+
const result = JSON.parse(data.content[0].text);
|
|
90
|
+
expect(result.echo).toBe("hello");
|
|
91
|
+
});
|
|
92
|
+
it("should return 404 for unknown tool", async () => {
|
|
93
|
+
const response = await fetch(`${baseUrl}/tools/unknown_tool`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ arguments: {} }),
|
|
97
|
+
});
|
|
98
|
+
expect(response.status).toBe(404);
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
expect(data.error).toContain("not found");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("SSE endpoint", () => {
|
|
104
|
+
it("should accept SSE connections", async () => {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeout = setTimeout(() => controller.abort(), 1000);
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(`${baseUrl}/sse`, {
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
expect(response.status).toBe(200);
|
|
112
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// AbortError is expected when we timeout
|
|
116
|
+
if (e instanceof Error && e.name !== "AbortError") {
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("BaseAccessServer helper methods", () => {
|
|
127
|
+
let server;
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
server = new TestServer();
|
|
130
|
+
});
|
|
131
|
+
describe("errorResponse", () => {
|
|
132
|
+
it("should create error response without hint", () => {
|
|
133
|
+
const result = server["errorResponse"]("Something went wrong");
|
|
134
|
+
expect(result.isError).toBe(true);
|
|
135
|
+
const textContent = result.content[0];
|
|
136
|
+
const content = JSON.parse(textContent.text);
|
|
137
|
+
expect(content.error).toBe("Something went wrong");
|
|
138
|
+
expect(content.hint).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
it("should create error response with hint", () => {
|
|
141
|
+
const result = server["errorResponse"]("Invalid input", "Try using a number");
|
|
142
|
+
expect(result.isError).toBe(true);
|
|
143
|
+
const textContent = result.content[0];
|
|
144
|
+
const content = JSON.parse(textContent.text);
|
|
145
|
+
expect(content.error).toBe("Invalid input");
|
|
146
|
+
expect(content.hint).toBe("Try using a number");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe("createJsonResource", () => {
|
|
150
|
+
it("should create JSON resource response", () => {
|
|
151
|
+
const result = server["createJsonResource"]("test://data", { foo: "bar" });
|
|
152
|
+
expect(result.contents).toHaveLength(1);
|
|
153
|
+
expect(result.contents[0].uri).toBe("test://data");
|
|
154
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
155
|
+
expect(JSON.parse(result.contents[0].text)).toEqual({ foo: "bar" });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe("createMarkdownResource", () => {
|
|
159
|
+
it("should create Markdown resource response", () => {
|
|
160
|
+
const result = server["createMarkdownResource"]("test://doc", "# Hello");
|
|
161
|
+
expect(result.contents).toHaveLength(1);
|
|
162
|
+
expect(result.contents[0].uri).toBe("test://doc");
|
|
163
|
+
expect(result.contents[0].mimeType).toBe("text/markdown");
|
|
164
|
+
expect(result.contents[0].text).toBe("# Hello");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("createTextResource", () => {
|
|
168
|
+
it("should create plain text resource response", () => {
|
|
169
|
+
const result = server["createTextResource"]("test://text", "Hello world");
|
|
170
|
+
expect(result.contents).toHaveLength(1);
|
|
171
|
+
expect(result.contents[0].uri).toBe("test://text");
|
|
172
|
+
expect(result.contents[0].mimeType).toBe("text/plain");
|
|
173
|
+
expect(result.contents[0].text).toBe("Hello world");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Integration tests for inter-server HTTP communication
|
|
6
|
+
* These tests start actual MCP servers and test tool execution via HTTP
|
|
7
|
+
*/
|
|
8
|
+
describe("Inter-server Communication Integration Tests", () => {
|
|
9
|
+
let nsfServer;
|
|
10
|
+
const NSF_PORT = 3199;
|
|
11
|
+
const NSF_URL = `http://localhost:${NSF_PORT}`;
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
// Start NSF Awards server in HTTP mode
|
|
14
|
+
const serverPath = path.resolve(process.cwd(), "packages/nsf-awards/dist/index.js");
|
|
15
|
+
nsfServer = spawn("node", [serverPath], {
|
|
16
|
+
env: {
|
|
17
|
+
...process.env,
|
|
18
|
+
PORT: String(NSF_PORT),
|
|
19
|
+
},
|
|
20
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
21
|
+
});
|
|
22
|
+
// Wait for server to be ready
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
const timeout = setTimeout(() => {
|
|
25
|
+
reject(new Error("Server startup timeout"));
|
|
26
|
+
}, 10000);
|
|
27
|
+
nsfServer.stdout?.on("data", (data) => {
|
|
28
|
+
if (data.toString().includes("HTTP server running")) {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
nsfServer.on("error", (err) => {
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
reject(err);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}, 15000);
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
if (nsfServer) {
|
|
41
|
+
nsfServer.kill("SIGTERM");
|
|
42
|
+
// Wait for process to exit
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
nsfServer.on("exit", () => resolve());
|
|
45
|
+
setTimeout(resolve, 1000); // Fallback timeout
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
describe("NSF Awards Server HTTP API", () => {
|
|
50
|
+
it("should respond to health check", async () => {
|
|
51
|
+
const response = await fetch(`${NSF_URL}/health`);
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
expect(response.status).toBe(200);
|
|
54
|
+
expect(data.status).toBe("healthy");
|
|
55
|
+
expect(data.server).toBe("access-mcp-nsf-awards");
|
|
56
|
+
});
|
|
57
|
+
it("should list available tools", async () => {
|
|
58
|
+
const response = await fetch(`${NSF_URL}/tools`);
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
expect(response.status).toBe(200);
|
|
61
|
+
expect(data.tools).toBeDefined();
|
|
62
|
+
expect(Array.isArray(data.tools)).toBe(true);
|
|
63
|
+
const toolNames = data.tools.map((t) => t.name);
|
|
64
|
+
expect(toolNames).toContain("search_nsf_awards");
|
|
65
|
+
});
|
|
66
|
+
it("should execute search_nsf_awards tool via HTTP", async () => {
|
|
67
|
+
const response = await fetch(`${NSF_URL}/tools/search_nsf_awards`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
arguments: {
|
|
72
|
+
keyword: "machine learning",
|
|
73
|
+
limit: 5,
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(data.content).toBeDefined();
|
|
80
|
+
expect(data.content[0].type).toBe("text");
|
|
81
|
+
// Parse the result
|
|
82
|
+
const result = JSON.parse(data.content[0].text);
|
|
83
|
+
expect(result).toBeDefined();
|
|
84
|
+
}, 15000);
|
|
85
|
+
it("should return 404 for unknown tool", async () => {
|
|
86
|
+
const response = await fetch(`${NSF_URL}/tools/nonexistent_tool`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ arguments: {} }),
|
|
90
|
+
});
|
|
91
|
+
expect(response.status).toBe(404);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("SSE Endpoint", () => {
|
|
95
|
+
it("should accept SSE connections on /sse", async () => {
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(`${NSF_URL}/sse`, {
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
});
|
|
102
|
+
expect(response.status).toBe(200);
|
|
103
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
// AbortError is expected
|
|
107
|
+
if (e instanceof Error && e.name !== "AbortError") {
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -8,8 +8,8 @@ describe("Utils", () => {
|
|
|
8
8
|
test("should keep valid characters", () => {
|
|
9
9
|
expect(sanitizeGroupId("test.group-123")).toBe("test.group-123");
|
|
10
10
|
});
|
|
11
|
-
test("should
|
|
12
|
-
expect(sanitizeGroupId("")).
|
|
11
|
+
test("should throw error for empty string", () => {
|
|
12
|
+
expect(() => sanitizeGroupId("")).toThrow("groupId parameter is required and cannot be null or undefined");
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
15
|
describe("formatApiUrl", () => {
|
|
@@ -38,12 +38,12 @@ describe("Utils", () => {
|
|
|
38
38
|
};
|
|
39
39
|
expect(handleApiError(error)).toBe("API error: 404 Not Found");
|
|
40
40
|
});
|
|
41
|
-
test("should handle
|
|
42
|
-
const error =
|
|
41
|
+
test("should handle Error instance", () => {
|
|
42
|
+
const error = new Error("Network error");
|
|
43
43
|
expect(handleApiError(error)).toBe("Network error");
|
|
44
44
|
});
|
|
45
45
|
test("should handle unknown error", () => {
|
|
46
|
-
const error =
|
|
46
|
+
const error = "some string error";
|
|
47
47
|
expect(handleApiError(error)).toBe("Unknown API error");
|
|
48
48
|
});
|
|
49
49
|
});
|
package/dist/base-server.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { Tool, Resource, Prompt, CallToolResult, ReadResourceResult, GetPromptResult, CallToolRequest, ReadResourceRequest, GetPromptRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import { AxiosInstance } from "axios";
|
|
5
|
+
export type { Tool, Resource, Prompt, CallToolResult, ReadResourceResult, GetPromptResult };
|
|
4
6
|
export declare abstract class BaseAccessServer {
|
|
5
7
|
protected serverName: string;
|
|
6
8
|
protected version: string;
|
|
@@ -10,30 +12,37 @@ export declare abstract class BaseAccessServer {
|
|
|
10
12
|
private _httpClient?;
|
|
11
13
|
private _httpServer?;
|
|
12
14
|
private _httpPort?;
|
|
15
|
+
private _sseTransports;
|
|
13
16
|
constructor(serverName: string, version: string, baseURL?: string);
|
|
14
17
|
protected get httpClient(): AxiosInstance;
|
|
15
18
|
private setupHandlers;
|
|
16
|
-
protected abstract getTools():
|
|
17
|
-
protected abstract getResources():
|
|
18
|
-
protected abstract handleToolCall(request:
|
|
19
|
+
protected abstract getTools(): Tool[];
|
|
20
|
+
protected abstract getResources(): Resource[];
|
|
21
|
+
protected abstract handleToolCall(request: CallToolRequest): Promise<CallToolResult>;
|
|
19
22
|
/**
|
|
20
23
|
* Get available prompts - override in subclasses to provide prompts
|
|
21
24
|
*/
|
|
22
|
-
protected getPrompts():
|
|
25
|
+
protected getPrompts(): Prompt[];
|
|
23
26
|
/**
|
|
24
27
|
* Handle resource read requests - override in subclasses
|
|
25
28
|
*/
|
|
26
|
-
protected handleResourceRead(request:
|
|
29
|
+
protected handleResourceRead(request: ReadResourceRequest): Promise<ReadResourceResult>;
|
|
27
30
|
/**
|
|
28
31
|
* Handle get prompt requests - override in subclasses
|
|
29
32
|
*/
|
|
30
|
-
protected handleGetPrompt(request:
|
|
33
|
+
protected handleGetPrompt(request: GetPromptRequest): Promise<GetPromptResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Helper method to create a standard error response (MCP 2025 compliant)
|
|
36
|
+
* @param message The error message
|
|
37
|
+
* @param hint Optional suggestion for how to fix the error
|
|
38
|
+
*/
|
|
39
|
+
protected errorResponse(message: string, hint?: string): CallToolResult;
|
|
31
40
|
/**
|
|
32
41
|
* Helper method to create a JSON resource response
|
|
33
42
|
* @param uri The resource URI
|
|
34
43
|
* @param data The data to return as JSON
|
|
35
44
|
*/
|
|
36
|
-
protected createJsonResource(uri: string, data:
|
|
45
|
+
protected createJsonResource(uri: string, data: unknown): {
|
|
37
46
|
contents: {
|
|
38
47
|
uri: string;
|
|
39
48
|
mimeType: string;
|
|
@@ -71,13 +80,17 @@ export declare abstract class BaseAccessServer {
|
|
|
71
80
|
httpPort?: number;
|
|
72
81
|
}): Promise<void>;
|
|
73
82
|
/**
|
|
74
|
-
* Start HTTP service layer for
|
|
83
|
+
* Start HTTP service layer with SSE support for remote MCP connections
|
|
75
84
|
*/
|
|
76
85
|
private startHttpService;
|
|
86
|
+
/**
|
|
87
|
+
* Set up MCP handlers on a server instance
|
|
88
|
+
*/
|
|
89
|
+
private setupServerHandlers;
|
|
77
90
|
/**
|
|
78
91
|
* Call a tool on another ACCESS-CI MCP server via HTTP
|
|
79
92
|
*/
|
|
80
|
-
protected callRemoteServer(serviceName: string, toolName: string, args?: Record<string,
|
|
93
|
+
protected callRemoteServer(serviceName: string, toolName: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
81
94
|
/**
|
|
82
95
|
* Get service endpoint from environment configuration
|
|
83
96
|
* Expected format: ACCESS_MCP_SERVICES=nsf-awards=http://localhost:3001,xdmod-metrics=http://localhost:3002
|
package/dist/base-server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
@@ -12,6 +13,7 @@ export class BaseAccessServer {
|
|
|
12
13
|
_httpClient;
|
|
13
14
|
_httpServer;
|
|
14
15
|
_httpPort;
|
|
16
|
+
_sseTransports = new Map();
|
|
15
17
|
constructor(serverName, version, baseURL = "https://support.access-ci.org/api") {
|
|
16
18
|
this.serverName = serverName;
|
|
17
19
|
this.version = version;
|
|
@@ -78,9 +80,12 @@ export class BaseAccessServer {
|
|
|
78
80
|
content: [
|
|
79
81
|
{
|
|
80
82
|
type: "text",
|
|
81
|
-
text:
|
|
83
|
+
text: JSON.stringify({
|
|
84
|
+
error: errorMessage
|
|
85
|
+
}),
|
|
82
86
|
},
|
|
83
87
|
],
|
|
88
|
+
isError: true,
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
});
|
|
@@ -132,13 +137,32 @@ export class BaseAccessServer {
|
|
|
132
137
|
* Handle resource read requests - override in subclasses
|
|
133
138
|
*/
|
|
134
139
|
async handleResourceRead(request) {
|
|
135
|
-
throw new Error(
|
|
140
|
+
throw new Error(`Resource reading not supported by this server: ${request.params.uri}`);
|
|
136
141
|
}
|
|
137
142
|
/**
|
|
138
143
|
* Handle get prompt requests - override in subclasses
|
|
139
144
|
*/
|
|
140
145
|
async handleGetPrompt(request) {
|
|
141
|
-
throw new Error(
|
|
146
|
+
throw new Error(`Prompt not found: ${request.params.name}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Helper method to create a standard error response (MCP 2025 compliant)
|
|
150
|
+
* @param message The error message
|
|
151
|
+
* @param hint Optional suggestion for how to fix the error
|
|
152
|
+
*/
|
|
153
|
+
errorResponse(message, hint) {
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: JSON.stringify({
|
|
159
|
+
error: message,
|
|
160
|
+
...(hint && { hint }),
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
isError: true,
|
|
165
|
+
};
|
|
142
166
|
}
|
|
143
167
|
/**
|
|
144
168
|
* Helper method to create a JSON resource response
|
|
@@ -206,7 +230,7 @@ export class BaseAccessServer {
|
|
|
206
230
|
}
|
|
207
231
|
}
|
|
208
232
|
/**
|
|
209
|
-
* Start HTTP service layer for
|
|
233
|
+
* Start HTTP service layer with SSE support for remote MCP connections
|
|
210
234
|
*/
|
|
211
235
|
async startHttpService() {
|
|
212
236
|
if (!this._httpPort)
|
|
@@ -222,7 +246,43 @@ export class BaseAccessServer {
|
|
|
222
246
|
timestamp: new Date().toISOString()
|
|
223
247
|
});
|
|
224
248
|
});
|
|
225
|
-
//
|
|
249
|
+
// SSE endpoint for MCP remote connections
|
|
250
|
+
this._httpServer.get('/sse', async (req, res) => {
|
|
251
|
+
console.log(`[${this.serverName}] New SSE connection`);
|
|
252
|
+
const transport = new SSEServerTransport('/messages', res);
|
|
253
|
+
const sessionId = transport.sessionId;
|
|
254
|
+
this._sseTransports.set(sessionId, transport);
|
|
255
|
+
// Clean up on disconnect
|
|
256
|
+
res.on('close', () => {
|
|
257
|
+
console.log(`[${this.serverName}] SSE connection closed: ${sessionId}`);
|
|
258
|
+
this._sseTransports.delete(sessionId);
|
|
259
|
+
});
|
|
260
|
+
// Create a new server instance for this SSE connection
|
|
261
|
+
const sseServer = new Server({
|
|
262
|
+
name: this.serverName,
|
|
263
|
+
version: this.version,
|
|
264
|
+
}, {
|
|
265
|
+
capabilities: {
|
|
266
|
+
resources: {},
|
|
267
|
+
tools: {},
|
|
268
|
+
prompts: {},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
// Set up handlers for the SSE server (same as main server)
|
|
272
|
+
this.setupServerHandlers(sseServer);
|
|
273
|
+
await sseServer.connect(transport);
|
|
274
|
+
});
|
|
275
|
+
// Messages endpoint for SSE POST messages
|
|
276
|
+
this._httpServer.post('/messages', async (req, res) => {
|
|
277
|
+
const sessionId = req.query.sessionId;
|
|
278
|
+
const transport = this._sseTransports.get(sessionId);
|
|
279
|
+
if (!transport) {
|
|
280
|
+
res.status(404).json({ error: 'Session not found' });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
284
|
+
});
|
|
285
|
+
// List available tools endpoint (for inter-server communication)
|
|
226
286
|
this._httpServer.get('/tools', (req, res) => {
|
|
227
287
|
try {
|
|
228
288
|
const tools = this.getTools();
|
|
@@ -232,7 +292,7 @@ export class BaseAccessServer {
|
|
|
232
292
|
res.status(500).json({ error: 'Failed to list tools' });
|
|
233
293
|
}
|
|
234
294
|
});
|
|
235
|
-
// Tool execution endpoint
|
|
295
|
+
// Tool execution endpoint (for inter-server communication)
|
|
236
296
|
this._httpServer.post('/tools/:toolName', async (req, res) => {
|
|
237
297
|
try {
|
|
238
298
|
const { toolName } = req.params;
|
|
@@ -241,10 +301,12 @@ export class BaseAccessServer {
|
|
|
241
301
|
const tools = this.getTools();
|
|
242
302
|
const tool = tools.find(t => t.name === toolName);
|
|
243
303
|
if (!tool) {
|
|
244
|
-
|
|
304
|
+
res.status(404).json({ error: `Tool '${toolName}' not found` });
|
|
305
|
+
return;
|
|
245
306
|
}
|
|
246
307
|
// Execute the tool
|
|
247
308
|
const request = {
|
|
309
|
+
method: "tools/call",
|
|
248
310
|
params: {
|
|
249
311
|
name: toolName,
|
|
250
312
|
arguments: args
|
|
@@ -265,6 +327,83 @@ export class BaseAccessServer {
|
|
|
265
327
|
}).on('error', reject);
|
|
266
328
|
});
|
|
267
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Set up MCP handlers on a server instance
|
|
332
|
+
*/
|
|
333
|
+
setupServerHandlers(server) {
|
|
334
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
335
|
+
try {
|
|
336
|
+
return { tools: this.getTools() };
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
return { tools: [] };
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
343
|
+
try {
|
|
344
|
+
return { resources: this.getResources() };
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return { resources: [] };
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
351
|
+
try {
|
|
352
|
+
return await this.handleToolCall(request);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
356
|
+
console.error("Error handling tool call:", errorMessage);
|
|
357
|
+
return {
|
|
358
|
+
content: [
|
|
359
|
+
{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: JSON.stringify({
|
|
362
|
+
error: errorMessage
|
|
363
|
+
}),
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
isError: true,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
371
|
+
try {
|
|
372
|
+
return await this.handleResourceRead(request);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
376
|
+
console.error("Error reading resource:", errorMessage);
|
|
377
|
+
return {
|
|
378
|
+
contents: [
|
|
379
|
+
{
|
|
380
|
+
uri: request.params.uri,
|
|
381
|
+
mimeType: "text/plain",
|
|
382
|
+
text: `Error: ${errorMessage}`,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
389
|
+
try {
|
|
390
|
+
return { prompts: this.getPrompts() };
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
return { prompts: [] };
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
397
|
+
try {
|
|
398
|
+
return await this.handleGetPrompt(request);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
402
|
+
console.error("Error getting prompt:", errorMessage);
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
268
407
|
/**
|
|
269
408
|
* Call a tool on another ACCESS-CI MCP server via HTTP
|
|
270
409
|
*/
|
|
@@ -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/types.d.ts
CHANGED
|
@@ -1,58 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
start_date?: string | undefined;
|
|
27
|
-
end_date?: string | undefined;
|
|
28
|
-
location?: string | undefined;
|
|
29
|
-
}, {
|
|
30
|
-
title: string;
|
|
31
|
-
id: string;
|
|
32
|
-
description?: string | undefined;
|
|
33
|
-
start_date?: string | undefined;
|
|
34
|
-
end_date?: string | undefined;
|
|
35
|
-
location?: string | undefined;
|
|
36
|
-
}>;
|
|
37
|
-
export declare const KnowledgeBaseResourceSchema: z.ZodObject<{
|
|
38
|
-
id: z.ZodString;
|
|
39
|
-
title: z.ZodString;
|
|
40
|
-
content: z.ZodOptional<z.ZodString>;
|
|
41
|
-
category: z.ZodOptional<z.ZodString>;
|
|
42
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
43
|
-
}, "strip", z.ZodTypeAny, {
|
|
44
|
-
title: string;
|
|
45
|
-
id: string;
|
|
46
|
-
content?: string | undefined;
|
|
47
|
-
category?: string | undefined;
|
|
48
|
-
tags?: string[] | undefined;
|
|
49
|
-
}, {
|
|
50
|
-
title: string;
|
|
51
|
-
id: string;
|
|
52
|
-
content?: string | undefined;
|
|
53
|
-
category?: string | undefined;
|
|
54
|
-
tags?: string[] | undefined;
|
|
55
|
-
}>;
|
|
56
|
-
export type AffinityGroup = z.infer<typeof AffinityGroupSchema>;
|
|
57
|
-
export type Event = z.infer<typeof EventSchema>;
|
|
58
|
-
export type KnowledgeBaseResource = z.infer<typeof KnowledgeBaseResourceSchema>;
|
|
1
|
+
export interface UniversalSearchParams {
|
|
2
|
+
query?: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
tags?: string[];
|
|
6
|
+
date?: "today" | "upcoming" | "past" | "this_week" | "this_month";
|
|
7
|
+
limit?: number;
|
|
8
|
+
offset?: number;
|
|
9
|
+
sort?: string;
|
|
10
|
+
order?: "asc" | "desc";
|
|
11
|
+
}
|
|
12
|
+
export interface UniversalResponse<T> {
|
|
13
|
+
total: number;
|
|
14
|
+
items: T[];
|
|
15
|
+
}
|
|
16
|
+
export interface UniversalResponseWithHints<T> extends UniversalResponse<T> {
|
|
17
|
+
hints?: {
|
|
18
|
+
next?: string;
|
|
19
|
+
tags?: string[];
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface StandardErrorResponse {
|
|
23
|
+
error: string;
|
|
24
|
+
hint?: string;
|
|
25
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1,21 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const AffinityGroupSchema = z.object({
|
|
3
|
-
group_id: z.string(),
|
|
4
|
-
name: z.string().optional(),
|
|
5
|
-
description: z.string().optional(),
|
|
6
|
-
});
|
|
7
|
-
export const EventSchema = z.object({
|
|
8
|
-
id: z.string(),
|
|
9
|
-
title: z.string(),
|
|
10
|
-
description: z.string().optional(),
|
|
11
|
-
start_date: z.string().optional(),
|
|
12
|
-
end_date: z.string().optional(),
|
|
13
|
-
location: z.string().optional(),
|
|
14
|
-
});
|
|
15
|
-
export const KnowledgeBaseResourceSchema = z.object({
|
|
16
|
-
id: z.string(),
|
|
17
|
-
title: z.string(),
|
|
18
|
-
content: z.string().optional(),
|
|
19
|
-
category: z.string().optional(),
|
|
20
|
-
tags: z.array(z.string()).optional(),
|
|
21
|
-
});
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
1
|
export declare function sanitizeGroupId(groupId: string): string;
|
|
2
2
|
export declare function formatApiUrl(version: string, endpoint: string): string;
|
|
3
|
-
export declare function handleApiError(error:
|
|
3
|
+
export declare function handleApiError(error: unknown): string;
|
|
4
|
+
/**
|
|
5
|
+
* LLM-Friendly Response Utilities
|
|
6
|
+
*
|
|
7
|
+
* These utilities help create responses that guide LLMs through multi-step workflows,
|
|
8
|
+
* provide clear next actions, and improve error handling.
|
|
9
|
+
*/
|
|
10
|
+
export interface NextStep {
|
|
11
|
+
action: string;
|
|
12
|
+
description: string;
|
|
13
|
+
tool?: string;
|
|
14
|
+
parameters?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface LLMResponse<T = unknown> {
|
|
17
|
+
data?: T;
|
|
18
|
+
count?: number;
|
|
19
|
+
next_steps?: NextStep[];
|
|
20
|
+
suggestions?: string[];
|
|
21
|
+
related_tools?: string[];
|
|
22
|
+
context?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface LLMError {
|
|
25
|
+
error: string;
|
|
26
|
+
error_type: "validation" | "not_found" | "api_error" | "invalid_parameter";
|
|
27
|
+
suggestions?: string[];
|
|
28
|
+
next_steps?: NextStep[];
|
|
29
|
+
did_you_mean?: string[];
|
|
30
|
+
related_queries?: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Add helpful next steps to a successful response
|
|
34
|
+
*/
|
|
35
|
+
export declare function addNextSteps<T>(data: T, nextSteps: NextStep[]): LLMResponse<T>;
|
|
36
|
+
/**
|
|
37
|
+
* Create an LLM-friendly error response with suggestions
|
|
38
|
+
*/
|
|
39
|
+
export declare function createLLMError(error: string, errorType: LLMError["error_type"], options?: {
|
|
40
|
+
suggestions?: string[];
|
|
41
|
+
nextSteps?: NextStep[];
|
|
42
|
+
didYouMean?: string[];
|
|
43
|
+
relatedQueries?: string[];
|
|
44
|
+
}): LLMError;
|
|
45
|
+
/**
|
|
46
|
+
* Add discovery suggestions when returning empty results
|
|
47
|
+
*/
|
|
48
|
+
export declare function addDiscoverySuggestions<T>(data: T[], discoverySteps: NextStep[]): LLMResponse<T[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Common next step templates for cross-server consistency
|
|
51
|
+
*/
|
|
52
|
+
export declare const CommonNextSteps: {
|
|
53
|
+
discoverResources: {
|
|
54
|
+
action: string;
|
|
55
|
+
description: string;
|
|
56
|
+
tool: string;
|
|
57
|
+
parameters: {
|
|
58
|
+
include_resource_ids: boolean;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
narrowResults: (currentCount: number, suggestedFilters: string[]) => {
|
|
62
|
+
action: string;
|
|
63
|
+
description: string;
|
|
64
|
+
};
|
|
65
|
+
exploreRelated: (relatedTool: string, description: string) => {
|
|
66
|
+
action: string;
|
|
67
|
+
description: string;
|
|
68
|
+
tool: string;
|
|
69
|
+
};
|
|
70
|
+
refineSearch: (suggestions: string[]) => {
|
|
71
|
+
action: string;
|
|
72
|
+
description: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
package/dist/utils.js
CHANGED
|
@@ -1,15 +1,84 @@
|
|
|
1
1
|
export function sanitizeGroupId(groupId) {
|
|
2
|
+
if (!groupId) {
|
|
3
|
+
throw new Error("groupId parameter is required and cannot be null or undefined");
|
|
4
|
+
}
|
|
2
5
|
return groupId.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
3
6
|
}
|
|
4
7
|
export function formatApiUrl(version, endpoint) {
|
|
5
8
|
return `/${version}/${endpoint}`;
|
|
6
9
|
}
|
|
7
10
|
export function handleApiError(error) {
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const axiosError = error;
|
|
12
|
+
if (axiosError.response?.data?.message) {
|
|
13
|
+
return axiosError.response.data.message;
|
|
14
|
+
}
|
|
15
|
+
if (axiosError.response?.status) {
|
|
16
|
+
return `API error: ${axiosError.response.status} ${axiosError.response.statusText}`;
|
|
17
|
+
}
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
return error.message;
|
|
10
20
|
}
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
return "Unknown API error";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Add helpful next steps to a successful response
|
|
25
|
+
*/
|
|
26
|
+
export function addNextSteps(data, nextSteps) {
|
|
27
|
+
return {
|
|
28
|
+
data,
|
|
29
|
+
next_steps: nextSteps,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create an LLM-friendly error response with suggestions
|
|
34
|
+
*/
|
|
35
|
+
export function createLLMError(error, errorType, options = {}) {
|
|
36
|
+
return {
|
|
37
|
+
error,
|
|
38
|
+
error_type: errorType,
|
|
39
|
+
...options,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Add discovery suggestions when returning empty results
|
|
44
|
+
*/
|
|
45
|
+
export function addDiscoverySuggestions(data, discoverySteps) {
|
|
46
|
+
if (data.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
data,
|
|
49
|
+
count: 0,
|
|
50
|
+
next_steps: discoverySteps,
|
|
51
|
+
suggestions: [
|
|
52
|
+
"No results found. Try the suggested next steps to discover available options.",
|
|
53
|
+
],
|
|
54
|
+
};
|
|
13
55
|
}
|
|
14
|
-
return
|
|
56
|
+
return {
|
|
57
|
+
data,
|
|
58
|
+
count: data.length,
|
|
59
|
+
};
|
|
15
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Common next step templates for cross-server consistency
|
|
63
|
+
*/
|
|
64
|
+
export const CommonNextSteps = {
|
|
65
|
+
discoverResources: {
|
|
66
|
+
action: "discover_resources",
|
|
67
|
+
description: "Find available compute resources to filter by",
|
|
68
|
+
tool: "search_resources",
|
|
69
|
+
parameters: { include_resource_ids: true },
|
|
70
|
+
},
|
|
71
|
+
narrowResults: (currentCount, suggestedFilters) => ({
|
|
72
|
+
action: "narrow_results",
|
|
73
|
+
description: `Currently showing ${currentCount} results. Add filters to narrow down: ${suggestedFilters.join(", ")}`,
|
|
74
|
+
}),
|
|
75
|
+
exploreRelated: (relatedTool, description) => ({
|
|
76
|
+
action: "explore_related",
|
|
77
|
+
description,
|
|
78
|
+
tool: relatedTool,
|
|
79
|
+
}),
|
|
80
|
+
refineSearch: (suggestions) => ({
|
|
81
|
+
action: "refine_search",
|
|
82
|
+
description: `Try these refinements: ${suggestions.join(", ")}`,
|
|
83
|
+
}),
|
|
84
|
+
};
|