Skip to content

Commit 979a07d

Browse files
committed
chore: add createSessionConfig hook MCP-294
Add a new async hook for library consumers to provide a configuration right before establishing an MCP session.
1 parent c3044df commit 979a07d

File tree

3 files changed

+177
-9
lines changed

3 files changed

+177
-9
lines changed

src/lib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export { ErrorCodes } from "./common/errors.js";
2121
export { Telemetry } from "./telemetry/telemetry.js";
2222
export { Keychain, registerGlobalSecretToRedact } from "./common/keychain.js";
2323
export type { Secret } from "./common/keychain.js";
24+
export type { TransportRunnerConfig } from "./transports/base.js";

src/transports/base.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ import { defaultCreateAtlasLocalClient } from "../common/atlasLocal.js";
2121
import type { Client } from "@mongodb-js/atlas-local";
2222
import { VectorSearchEmbeddingsManager } from "../common/search/vectorSearchEmbeddingsManager.js";
2323

24+
type CreateSessionConfigFn = (userConfig: UserConfig) => Promise<UserConfig>;
25+
2426
export type TransportRunnerConfig = {
2527
userConfig: UserConfig;
2628
createConnectionManager?: ConnectionManagerFactoryFn;
2729
connectionErrorHandler?: ConnectionErrorHandler;
2830
createAtlasLocalClient?: AtlasLocalClientFactoryFn;
2931
additionalLoggers?: LoggerBase[];
3032
telemetryProperties?: Partial<CommonProperties>;
33+
/**
34+
* Hook which allows library consumers to fetch configuration from external sources (e.g., secrets managers, APIs)
35+
* or modify the existing configuration before the session is created.
36+
*/
37+
createSessionConfig?: CreateSessionConfigFn;
3138
};
3239

3340
export abstract class TransportRunnerBase {
@@ -38,6 +45,7 @@ export abstract class TransportRunnerBase {
3845
private readonly connectionErrorHandler: ConnectionErrorHandler;
3946
private readonly atlasLocalClient: Promise<Client | undefined>;
4047
private readonly telemetryProperties: Partial<CommonProperties>;
48+
private readonly createSessionConfig?: CreateSessionConfigFn;
4149

4250
protected constructor({
4351
userConfig,
@@ -46,12 +54,14 @@ export abstract class TransportRunnerBase {
4654
createAtlasLocalClient = defaultCreateAtlasLocalClient,
4755
additionalLoggers = [],
4856
telemetryProperties = {},
57+
createSessionConfig,
4958
}: TransportRunnerConfig) {
5059
this.userConfig = userConfig;
5160
this.createConnectionManager = createConnectionManager;
5261
this.connectionErrorHandler = connectionErrorHandler;
5362
this.atlasLocalClient = createAtlasLocalClient();
5463
this.telemetryProperties = telemetryProperties;
64+
this.createSessionConfig = createSessionConfig;
5565
const loggers: LoggerBase[] = [...additionalLoggers];
5666
if (this.userConfig.loggers.includes("stderr")) {
5767
loggers.push(new ConsoleLogger(Keychain.root));
@@ -76,32 +86,36 @@ export abstract class TransportRunnerBase {
7686
}
7787

7888
protected async setupServer(): Promise<Server> {
89+
// Call the config provider hook if provided, allowing consumers to
90+
// fetch or modify configuration before session initialization
91+
const userConfig = this.createSessionConfig ? await this.createSessionConfig(this.userConfig) : this.userConfig;
92+
7993
const mcpServer = new McpServer({
8094
name: packageInfo.mcpServerName,
8195
version: packageInfo.version,
8296
});
8397

8498
const logger = new CompositeLogger(this.logger);
85-
const exportsManager = ExportsManager.init(this.userConfig, logger);
99+
const exportsManager = ExportsManager.init(userConfig, logger);
86100
const connectionManager = await this.createConnectionManager({
87101
logger,
88-
userConfig: this.userConfig,
102+
userConfig,
89103
deviceId: this.deviceId,
90104
});
91105

92106
const session = new Session({
93-
apiBaseUrl: this.userConfig.apiBaseUrl,
94-
apiClientId: this.userConfig.apiClientId,
95-
apiClientSecret: this.userConfig.apiClientSecret,
107+
apiBaseUrl: userConfig.apiBaseUrl,
108+
apiClientId: userConfig.apiClientId,
109+
apiClientSecret: userConfig.apiClientSecret,
96110
atlasLocalClient: await this.atlasLocalClient,
97111
logger,
98112
exportsManager,
99113
connectionManager,
100114
keychain: Keychain.root,
101-
vectorSearchEmbeddingsManager: new VectorSearchEmbeddingsManager(this.userConfig, connectionManager),
115+
vectorSearchEmbeddingsManager: new VectorSearchEmbeddingsManager(userConfig, connectionManager),
102116
});
103117

104-
const telemetry = Telemetry.create(session, this.userConfig, this.deviceId, {
118+
const telemetry = Telemetry.create(session, userConfig, this.deviceId, {
105119
commonProperties: this.telemetryProperties,
106120
});
107121

@@ -111,14 +125,14 @@ export abstract class TransportRunnerBase {
111125
mcpServer,
112126
session,
113127
telemetry,
114-
userConfig: this.userConfig,
128+
userConfig,
115129
connectionErrorHandler: this.connectionErrorHandler,
116130
elicitation,
117131
});
118132

119133
// We need to create the MCP logger after the server is constructed
120134
// because it needs the server instance
121-
if (this.userConfig.loggers.includes("mcp")) {
135+
if (userConfig.loggers.includes("mcp")) {
122136
logger.addLogger(new McpLogger(result, Keychain.root));
123137
}
124138

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { StreamableHttpRunner } from "../../../src/transports/streamableHttp.js";
2+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4+
import { describe, expect, it, beforeAll, afterAll } from "vitest";
5+
import type { UserConfig } from "../../../src/common/config.js";
6+
import { TransportRunnerConfig } from "../../../src/lib.js";
7+
import { defaultTestConfig } from "../helpers.js";
8+
9+
describe("createSessionConfig", () => {
10+
const userConfig = defaultTestConfig;
11+
let runner: StreamableHttpRunner;
12+
13+
describe("basic functionality", () => {
14+
it("should use the modified config from createSessionConfig", async () => {
15+
const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async (userConfig) => {
16+
return {
17+
...userConfig,
18+
apiBaseUrl: "https://test-api.mongodb.com/",
19+
};
20+
};
21+
22+
userConfig.httpPort = 0; // Use a random port
23+
runner = new StreamableHttpRunner({
24+
userConfig,
25+
createSessionConfig,
26+
});
27+
await runner.start();
28+
29+
const server = await runner["setupServer"]();
30+
expect(server.userConfig.apiBaseUrl).toBe("https://test-api.mongodb.com/");
31+
32+
await runner.close();
33+
});
34+
35+
it("should work without a createSessionConfig", async () => {
36+
userConfig.httpPort = 0; // Use a random port
37+
runner = new StreamableHttpRunner({
38+
userConfig,
39+
});
40+
await runner.start();
41+
42+
const server = await runner["setupServer"]();
43+
expect(server.userConfig.apiBaseUrl).toBe(userConfig.apiBaseUrl);
44+
45+
await runner.close();
46+
});
47+
});
48+
49+
describe("connection string modification", () => {
50+
it("should allow modifying connection string via createSessionConfig", async () => {
51+
const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async (userConfig) => {
52+
// Simulate fetching connection string from environment or secrets
53+
await new Promise((resolve) => setTimeout(resolve, 10));
54+
55+
return {
56+
...userConfig,
57+
connectionString: "mongodb://test-server:27017/test-db",
58+
};
59+
};
60+
61+
userConfig.httpPort = 0; // Use a random port
62+
runner = new StreamableHttpRunner({
63+
userConfig: { ...userConfig, connectionString: undefined },
64+
createSessionConfig,
65+
});
66+
await runner.start();
67+
68+
const server = await runner["setupServer"]();
69+
expect(server.userConfig.connectionString).toBe("mongodb://test-server:27017/test-db");
70+
71+
await runner.close();
72+
});
73+
});
74+
75+
describe("server integration", () => {
76+
let client: Client;
77+
let transport: StreamableHTTPClientTransport;
78+
79+
it("should successfully initialize server with createSessionConfig and serve requests", async () => {
80+
const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async (userConfig) => {
81+
// Simulate async config modification
82+
await new Promise((resolve) => setTimeout(resolve, 10));
83+
return {
84+
...userConfig,
85+
readOnly: true, // Enable read-only mode
86+
};
87+
};
88+
89+
userConfig.httpPort = 0; // Use a random port
90+
runner = new StreamableHttpRunner({
91+
userConfig,
92+
createSessionConfig,
93+
});
94+
await runner.start();
95+
96+
client = new Client({
97+
name: "test-client",
98+
version: "1.0.0",
99+
});
100+
transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`));
101+
102+
await client.connect(transport);
103+
const response = await client.listTools();
104+
105+
expect(response).toBeDefined();
106+
expect(response.tools).toBeDefined();
107+
expect(response.tools.length).toBeGreaterThan(0);
108+
109+
// Verify read-only mode is applied - insert-one should not be available
110+
const writeTools = response.tools.filter((tool) => tool.name === "insert-one");
111+
expect(writeTools.length).toBe(0);
112+
113+
// Verify read tools are available
114+
const readTools = response.tools.filter((tool) => tool.name === "find");
115+
expect(readTools.length).toBe(1);
116+
117+
await client.close();
118+
await transport.close();
119+
await runner.close();
120+
});
121+
});
122+
123+
describe("error handling", () => {
124+
it("should propagate errors from configProvider on client connection", async () => {
125+
const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async () => {
126+
throw new Error("Failed to fetch config");
127+
};
128+
129+
userConfig.httpPort = 0; // Use a random port
130+
runner = new StreamableHttpRunner({
131+
userConfig,
132+
createSessionConfig,
133+
});
134+
135+
// Start succeeds because setupServer is only called when a client connects
136+
await runner.start();
137+
138+
// Error should occur when a client tries to connect
139+
const testClient = new Client({
140+
name: "test-client",
141+
version: "1.0.0",
142+
});
143+
const testTransport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`));
144+
145+
await expect(testClient.connect(testTransport)).rejects.toThrow();
146+
147+
await testClient.close();
148+
await testTransport.close();
149+
150+
await runner.close();
151+
});
152+
});
153+
});

0 commit comments

Comments
 (0)