Skip to content

Commit 1281929

Browse files
committed
fix(broker,gateway,adapter): wire resolve route, fix ESM imports, add test harness
Changes: 1. nexus-broker: Register missing GET /connections/resolve route in main.go 2. nexus-gateway: Forward broker error bodies on 4xx resolve responses 3. nexus-mcp-adapter: Fix verbatimModuleSyntax compliance (import type, .js extensions) 4. nexus-mcp-adapter: Add 5 MCP test servers (github, slack, notion, salesforce, google) and parallel test runner All broker and gateway tests pass. go vet clean.
1 parent f9c0e41 commit 1281929

11 files changed

Lines changed: 582 additions & 6 deletions

File tree

nexus-broker/cmd/nexus-broker/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func main() {
116116
r.Delete("/{id}", providersHandler.Delete)
117117
})
118118
protected.Post("/auth/consent-spec", consentHandler.GetSpec)
119+
protected.Get("/connections/resolve", callbackHandler.ResolveToken)
119120
protected.Get("/connections/{connectionID}/token", callbackHandler.GetToken)
120121
protected.Post("/connections/{connectionID}/refresh", callbackHandler.Refresh)
121122

nexus-gateway/pkg/usecase/handler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,11 @@ func (h *Handler) ResolveToken(w http.ResponseWriter, r *http.Request) {
439439
}
440440

441441
if resp.StatusCode() >= 400 {
442+
w.Header().Set("Content-Type", "application/json")
442443
w.WriteHeader(resp.StatusCode())
444+
if resp.Body != nil {
445+
_, _ = w.Write(resp.Body)
446+
}
443447
return
444448
}
445449

nexus-mcp-adapter/src/NexusClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { TokenManager } from './TokenManager';
2-
import { FetcherOptions, NexusClientOptions, NexusTokenInfo } from './types';
1+
import { TokenManager } from './TokenManager.js';
2+
import type { FetcherOptions, NexusClientOptions, NexusTokenInfo } from './types.js';
33

44
export class NexusClient {
55
private gatewayUrl: string;

nexus-mcp-adapter/src/TokenManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { LRUCache } from 'lru-cache';
2-
import { NexusTokenInfo } from './types';
2+
import type { NexusTokenInfo } from './types.js';
33

44
export class TokenManager {
55
private cache: LRUCache<string, NexusTokenInfo>;

nexus-mcp-adapter/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export * from './types';
2-
export { NexusClient } from './NexusClient';
3-
export { TokenManager } from './TokenManager';
1+
export type { NexusClientOptions, NexusTokenInfo, FetcherOptions } from './types.js';
2+
export { NexusClient } from './NexusClient.js';
3+
export { TokenManager } from './TokenManager.js';

nexus-mcp-adapter/tests/runner.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* Nexus MCP Adapter — Test Runner
3+
*
4+
* Spawns 5 MCP servers via stdio, connects an MCP client to each,
5+
* invokes their tool, and reports pass/fail.
6+
*
7+
* Prerequisites:
8+
* 1. Nexus stack running locally (make up)
9+
* 2. Active connections in the broker for the test workspace_id
10+
* for each provider being tested.
11+
*
12+
* Usage:
13+
* npx tsx tests/runner.ts [--workspace <id>] [--providers github,slack,notion,salesforce,google]
14+
*/
15+
16+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
18+
import path from "path";
19+
import { fileURLToPath } from "url";
20+
21+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
22+
23+
// --- Configuration ---
24+
25+
const WORKSPACE_ID = process.env.NEXUS_TEST_WORKSPACE || parseArg("--workspace") || "test-workspace-001";
26+
27+
interface ServerConfig {
28+
name: string;
29+
script: string;
30+
tool: string;
31+
args: Record<string, string>;
32+
}
33+
34+
const ALL_SERVERS: ServerConfig[] = [
35+
{
36+
name: "github",
37+
script: "servers/github.ts",
38+
tool: "github_list_repos",
39+
args: { workspace_id: WORKSPACE_ID },
40+
},
41+
{
42+
name: "slack",
43+
script: "servers/slack.ts",
44+
tool: "slack_list_channels",
45+
args: { workspace_id: WORKSPACE_ID },
46+
},
47+
{
48+
name: "notion",
49+
script: "servers/notion.ts",
50+
tool: "notion_search",
51+
args: { workspace_id: WORKSPACE_ID, query: "test" },
52+
},
53+
{
54+
name: "salesforce",
55+
script: "servers/salesforce.ts",
56+
tool: "salesforce_query",
57+
args: { workspace_id: WORKSPACE_ID, soql: "SELECT Id, Name FROM Account LIMIT 5" },
58+
},
59+
{
60+
name: "google",
61+
script: "servers/google.ts",
62+
tool: "google_userinfo",
63+
args: { workspace_id: WORKSPACE_ID },
64+
},
65+
];
66+
67+
// Allow filtering providers via CLI: --providers github,slack
68+
const providerFilter = parseArg("--providers");
69+
const selectedProviders = providerFilter
70+
? providerFilter.split(",").map((p) => p.trim().toLowerCase())
71+
: ALL_SERVERS.map((s) => s.name);
72+
73+
const servers = ALL_SERVERS.filter((s) => selectedProviders.includes(s.name));
74+
75+
// --- Helpers ---
76+
77+
function parseArg(flag: string): string | undefined {
78+
const idx = process.argv.indexOf(flag);
79+
if (idx !== -1 && idx + 1 < process.argv.length) {
80+
return process.argv[idx + 1];
81+
}
82+
return undefined;
83+
}
84+
85+
interface TestResult {
86+
server: string;
87+
passed: boolean;
88+
detail: string;
89+
durationMs: number;
90+
}
91+
92+
async function testServer(config: ServerConfig): Promise<TestResult> {
93+
const start = Date.now();
94+
const scriptPath = path.resolve(__dirname, config.script);
95+
96+
let client: Client | null = null;
97+
let transport: StdioClientTransport | null = null;
98+
99+
try {
100+
// 1. Spawn the MCP server as a child process
101+
transport = new StdioClientTransport({
102+
command: "npx",
103+
args: ["tsx", scriptPath],
104+
env: { ...process.env },
105+
});
106+
107+
client = new Client({ name: `test-client-${config.name}`, version: "1.0.0" });
108+
await client.connect(transport);
109+
110+
// 2. List tools to verify registration
111+
const toolsResult = await client.listTools();
112+
const toolNames = toolsResult.tools.map((t) => t.name);
113+
if (!toolNames.includes(config.tool)) {
114+
return {
115+
server: config.name,
116+
passed: false,
117+
detail: `Tool '${config.tool}' not found. Available: [${toolNames.join(", ")}]`,
118+
durationMs: Date.now() - start,
119+
};
120+
}
121+
122+
// 3. Call the tool
123+
const result = await client.callTool({ name: config.tool, arguments: config.args });
124+
125+
// 4. Check result
126+
const content = result.content as any[];
127+
const isError = result.isError === true;
128+
const text = content?.[0]?.text || "";
129+
130+
if (isError) {
131+
return {
132+
server: config.name,
133+
passed: false,
134+
detail: text.substring(0, 200),
135+
durationMs: Date.now() - start,
136+
};
137+
}
138+
139+
// Try to parse as JSON to get a summary
140+
let summary = text.substring(0, 100);
141+
try {
142+
const parsed = JSON.parse(text);
143+
if (Array.isArray(parsed)) {
144+
summary = `returned ${parsed.length} items`;
145+
} else if (parsed.totalSize !== undefined) {
146+
summary = `returned ${parsed.totalSize} records`;
147+
} else if (parsed.name) {
148+
summary = `user: ${parsed.name} (${parsed.email})`;
149+
} else {
150+
summary = `returned ${Object.keys(parsed).length} fields`;
151+
}
152+
} catch {
153+
summary = text.substring(0, 80);
154+
}
155+
156+
return {
157+
server: config.name,
158+
passed: true,
159+
detail: summary,
160+
durationMs: Date.now() - start,
161+
};
162+
} catch (error: any) {
163+
return {
164+
server: config.name,
165+
passed: false,
166+
detail: error.message?.substring(0, 200) || "Unknown error",
167+
durationMs: Date.now() - start,
168+
};
169+
} finally {
170+
try {
171+
await client?.close();
172+
} catch { /* ignore cleanup errors */ }
173+
try {
174+
await transport?.close();
175+
} catch { /* ignore */ }
176+
}
177+
}
178+
179+
// --- Main ---
180+
181+
async function main() {
182+
console.log("╔══════════════════════════════════════════════════════╗");
183+
console.log("║ Nexus MCP Adapter — Integration Test Suite ║");
184+
console.log("╚══════════════════════════════════════════════════════╝");
185+
console.log();
186+
console.log(` Workspace: ${WORKSPACE_ID}`);
187+
console.log(` Gateway: ${process.env.NEXUS_GATEWAY_URL || "http://localhost:8090"}`);
188+
console.log(` Servers: ${servers.map((s) => s.name).join(", ")}`);
189+
console.log();
190+
191+
// Run all servers in parallel
192+
console.log("Running tests...\n");
193+
const results = await Promise.allSettled(servers.map((s) => testServer(s)));
194+
195+
// Print results
196+
let passed = 0;
197+
let failed = 0;
198+
199+
for (const result of results) {
200+
if (result.status === "fulfilled") {
201+
const r = result.value;
202+
const icon = r.passed ? "✅" : "❌";
203+
const status = r.passed ? "PASS" : "FAIL";
204+
console.log(` ${icon} [${r.server.padEnd(12)}] ${status} (${r.durationMs}ms) — ${r.detail}`);
205+
r.passed ? passed++ : failed++;
206+
} else {
207+
console.log(` ❌ [unknown ] FAIL — Promise rejected: ${result.reason}`);
208+
failed++;
209+
}
210+
}
211+
212+
console.log();
213+
console.log(` Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
214+
console.log();
215+
216+
if (failed > 0) {
217+
console.log(" Some tests failed. Ensure active connections exist for the test workspace.");
218+
console.log(" Run the consent flow for each provider first:");
219+
console.log(` curl -X POST http://localhost:8090/v1/request-connection \\`);
220+
console.log(` -H "Content-Type: application/json" \\`);
221+
console.log(` -d '{"user_id":"${WORKSPACE_ID}","provider_name":"<provider>","scopes":["..."],"return_url":"http://localhost:3000/callback"}'`);
222+
process.exit(1);
223+
}
224+
225+
console.log(" All tests passed! 🎉");
226+
process.exit(0);
227+
}
228+
229+
main().catch((err) => {
230+
console.error("Fatal runner error:", err);
231+
process.exit(1);
232+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3+
import { z } from "zod";
4+
import { NexusClient } from "../../src/index.js";
5+
6+
const GATEWAY_URL = process.env.NEXUS_GATEWAY_URL || "http://localhost:8090";
7+
const API_KEY = process.env.NEXUS_API_KEY || "nexus-admin-key";
8+
9+
const nexus = new NexusClient({ gatewayUrl: GATEWAY_URL, apiKey: API_KEY });
10+
11+
const server = new McpServer({
12+
name: "nexus-test-github",
13+
version: "1.0.0",
14+
});
15+
16+
server.tool(
17+
"github_list_repos",
18+
"List the authenticated user's GitHub repositories",
19+
{
20+
workspace_id: z.string().describe("The Nexus Workspace ID"),
21+
},
22+
async (args) => {
23+
try {
24+
const authedFetch = nexus.createFetcher({
25+
provider: "github",
26+
workspaceId: args.workspace_id,
27+
});
28+
29+
const response = await authedFetch("https://api.github.com/user/repos?per_page=5&sort=updated", {
30+
headers: {
31+
"Accept": "application/vnd.github.v3+json",
32+
"User-Agent": "Nexus-MCP-Test",
33+
},
34+
});
35+
36+
if (!response.ok) {
37+
const errText = await response.text();
38+
throw new Error(`GitHub API ${response.status}: ${errText}`);
39+
}
40+
41+
const repos = await response.json() as any[];
42+
const summary = repos.map((r: any) => ({
43+
name: r.full_name,
44+
stars: r.stargazers_count,
45+
language: r.language,
46+
}));
47+
48+
return {
49+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
50+
};
51+
} catch (error: any) {
52+
return {
53+
isError: true,
54+
content: [{ type: "text", text: `GitHub error: ${error.message}` }],
55+
};
56+
}
57+
}
58+
);
59+
60+
async function run() {
61+
const transport = new StdioServerTransport();
62+
await server.connect(transport);
63+
console.error("[github-server] Running on stdio");
64+
}
65+
66+
run().catch(console.error);

0 commit comments

Comments
 (0)