@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.
@@ -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,118 @@
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
+ LOG_LEVEL: "info", // Enable info logging to see startup message
20
+ },
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ });
23
+ // Wait for server to be ready
24
+ await new Promise((resolve, reject) => {
25
+ const timeout = setTimeout(() => {
26
+ reject(new Error("Server startup timeout"));
27
+ }, 10000);
28
+ // Logger writes to stderr, not stdout
29
+ nsfServer.stderr?.on("data", (data) => {
30
+ if (data.toString().includes("HTTP server running")) {
31
+ clearTimeout(timeout);
32
+ resolve();
33
+ }
34
+ });
35
+ nsfServer.on("error", (err) => {
36
+ clearTimeout(timeout);
37
+ reject(err);
38
+ });
39
+ });
40
+ }, 15000);
41
+ afterAll(async () => {
42
+ if (nsfServer) {
43
+ nsfServer.kill("SIGTERM");
44
+ // Wait for process to exit
45
+ await new Promise((resolve) => {
46
+ nsfServer.on("exit", () => resolve());
47
+ setTimeout(resolve, 1000); // Fallback timeout
48
+ });
49
+ }
50
+ });
51
+ describe("NSF Awards Server HTTP API", () => {
52
+ it("should respond to health check", async () => {
53
+ const response = await fetch(`${NSF_URL}/health`);
54
+ const data = await response.json();
55
+ expect(response.status).toBe(200);
56
+ expect(data.status).toBe("healthy");
57
+ expect(data.server).toBe("access-mcp-nsf-awards");
58
+ });
59
+ it("should list available tools", async () => {
60
+ const response = await fetch(`${NSF_URL}/tools`);
61
+ const data = await response.json();
62
+ expect(response.status).toBe(200);
63
+ expect(data.tools).toBeDefined();
64
+ expect(Array.isArray(data.tools)).toBe(true);
65
+ const toolNames = data.tools.map((t) => t.name);
66
+ expect(toolNames).toContain("search_nsf_awards");
67
+ });
68
+ it("should execute search_nsf_awards tool via HTTP", async () => {
69
+ const response = await fetch(`${NSF_URL}/tools/search_nsf_awards`, {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({
73
+ arguments: {
74
+ keyword: "machine learning",
75
+ limit: 5,
76
+ },
77
+ }),
78
+ });
79
+ const data = await response.json();
80
+ expect(response.status).toBe(200);
81
+ expect(data.content).toBeDefined();
82
+ expect(data.content[0].type).toBe("text");
83
+ // Parse the result
84
+ const result = JSON.parse(data.content[0].text);
85
+ expect(result).toBeDefined();
86
+ }, 15000);
87
+ it("should return 404 for unknown tool", async () => {
88
+ const response = await fetch(`${NSF_URL}/tools/nonexistent_tool`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify({ arguments: {} }),
92
+ });
93
+ expect(response.status).toBe(404);
94
+ });
95
+ });
96
+ describe("SSE Endpoint", () => {
97
+ it("should accept SSE connections on /sse", async () => {
98
+ const controller = new AbortController();
99
+ const timeout = setTimeout(() => controller.abort(), 2000);
100
+ try {
101
+ const response = await fetch(`${NSF_URL}/sse`, {
102
+ signal: controller.signal,
103
+ });
104
+ expect(response.status).toBe(200);
105
+ expect(response.headers.get("content-type")).toContain("text/event-stream");
106
+ }
107
+ catch (e) {
108
+ // AbortError is expected
109
+ if (e instanceof Error && e.name !== "AbortError") {
110
+ throw e;
111
+ }
112
+ }
113
+ finally {
114
+ clearTimeout(timeout);
115
+ }
116
+ });
117
+ });
118
+ });
@@ -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 error message", () => {
42
- const error = { message: "Network 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
  });
@@ -1,51 +1,50 @@
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
+ import { Logger } from "./logger.js";
6
+ export type { Tool, Resource, Prompt, CallToolResult, ReadResourceResult, GetPromptResult };
4
7
  export declare abstract class BaseAccessServer {
5
8
  protected serverName: string;
6
9
  protected version: string;
7
10
  protected baseURL: string;
8
11
  protected server: Server;
9
12
  protected transport: StdioServerTransport;
13
+ protected logger: Logger;
10
14
  private _httpClient?;
11
15
  private _httpServer?;
12
16
  private _httpPort?;
17
+ private _sseTransports;
13
18
  constructor(serverName: string, version: string, baseURL?: string);
14
19
  protected get httpClient(): AxiosInstance;
15
20
  private setupHandlers;
16
- protected abstract getTools(): any[];
17
- protected abstract getResources(): any[];
18
- protected abstract handleToolCall(request: any): Promise<any>;
21
+ protected abstract getTools(): Tool[];
22
+ protected abstract getResources(): Resource[];
23
+ protected abstract handleToolCall(request: CallToolRequest): Promise<CallToolResult>;
19
24
  /**
20
25
  * Get available prompts - override in subclasses to provide prompts
21
26
  */
22
- protected getPrompts(): any[];
27
+ protected getPrompts(): Prompt[];
23
28
  /**
24
29
  * Handle resource read requests - override in subclasses
25
30
  */
26
- protected handleResourceRead(request: any): Promise<any>;
31
+ protected handleResourceRead(request: ReadResourceRequest): Promise<ReadResourceResult>;
27
32
  /**
28
33
  * Handle get prompt requests - override in subclasses
29
34
  */
30
- protected handleGetPrompt(request: any): Promise<any>;
35
+ protected handleGetPrompt(request: GetPromptRequest): Promise<GetPromptResult>;
31
36
  /**
32
37
  * Helper method to create a standard error response (MCP 2025 compliant)
33
38
  * @param message The error message
34
39
  * @param hint Optional suggestion for how to fix the error
35
40
  */
36
- protected errorResponse(message: string, hint?: string): {
37
- content: {
38
- type: "text";
39
- text: string;
40
- }[];
41
- isError: boolean;
42
- };
41
+ protected errorResponse(message: string, hint?: string): CallToolResult;
43
42
  /**
44
43
  * Helper method to create a JSON resource response
45
44
  * @param uri The resource URI
46
45
  * @param data The data to return as JSON
47
46
  */
48
- protected createJsonResource(uri: string, data: any): {
47
+ protected createJsonResource(uri: string, data: unknown): {
49
48
  contents: {
50
49
  uri: string;
51
50
  mimeType: string;
@@ -83,13 +82,17 @@ export declare abstract class BaseAccessServer {
83
82
  httpPort?: number;
84
83
  }): Promise<void>;
85
84
  /**
86
- * Start HTTP service layer for inter-server communication
85
+ * Start HTTP service layer with SSE support for remote MCP connections
87
86
  */
88
87
  private startHttpService;
88
+ /**
89
+ * Set up MCP handlers on a server instance
90
+ */
91
+ private setupServerHandlers;
89
92
  /**
90
93
  * Call a tool on another ACCESS-CI MCP server via HTTP
91
94
  */
92
- protected callRemoteServer(serviceName: string, toolName: string, args?: Record<string, any>): Promise<any>;
95
+ protected callRemoteServer(serviceName: string, toolName: string, args?: Record<string, unknown>): Promise<unknown>;
93
96
  /**
94
97
  * Get service endpoint from environment configuration
95
98
  * Expected format: ACCESS_MCP_SERVICES=nsf-awards=http://localhost:3001,xdmod-metrics=http://localhost:3002