Skip to content

Commit 401d064

Browse files
committed
feat: sigv4 streamable HTTP transport client - Typescript implementation
1 parent 07988db commit 401d064

File tree

16 files changed

+1252
-78
lines changed

16 files changed

+1252
-78
lines changed

e2e_tests/typescript/src/main.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { ChatSession } from "./chat_session.js";
33
import { LLMClient } from "./llm_client.js";
44
import { StdioServer } from "./server_clients/stdio_server.js";
55
import { LambdaFunctionClient } from "./server_clients/lambda_function.js";
6+
import {
7+
LambdaFunctionUrlClient,
8+
LambdaFunctionUrlConfig,
9+
} from "./server_clients/lambda_function_url.js";
610
import {
711
AutomatedOAuthClient,
812
AutomatedOAuthConfig,
@@ -33,6 +37,15 @@ async function main(): Promise<void> {
3337
);
3438
}
3539

40+
// Initialize Lambda function URL servers
41+
for (const [name, srvConfig] of Object.entries(
42+
serverConfig.lambdaFunctionUrls || {}
43+
)) {
44+
servers.push(
45+
new LambdaFunctionUrlClient(name, srvConfig as LambdaFunctionUrlConfig)
46+
);
47+
}
48+
3649
// Initialize automated OAuth servers
3750
for (const [name, srvConfig] of Object.entries(
3851
serverConfig.oAuthServers || {}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
CloudFormationClient,
3+
DescribeStacksCommand,
4+
} from "@aws-sdk/client-cloudformation";
5+
import { StreamableHTTPClientWithSigV4Transport } from "@aws/run-mcp-servers-with-aws-lambda";
6+
import { Server } from "./server.js";
7+
import logger from "../logger.js";
8+
9+
/**
10+
* Configuration for LambdaFunctionUrlClient
11+
*/
12+
export interface LambdaFunctionUrlConfig {
13+
/**
14+
* Direct Lambda function URL (if provided, stackName should not be used)
15+
*/
16+
functionUrl?: string;
17+
18+
/**
19+
* CloudFormation stack name to lookup function URL from (if provided, functionUrl should not be used)
20+
*/
21+
stackName?: string;
22+
23+
/**
24+
* CloudFormation stack output key for the function URL (default: "FunctionUrl")
25+
*/
26+
stackUrlOutputKey?: string;
27+
28+
/**
29+
* AWS region (default: "us-east-2")
30+
*/
31+
region?: string;
32+
}
33+
34+
/**
35+
* Manages MCP server connections and tool execution for servers running behind
36+
* Lambda function URLs with AWS SigV4 authentication.
37+
*
38+
* This client can lookup the function URL from a CloudFormation stack output
39+
* instead of requiring the user to statically configure the URL.
40+
*/
41+
export class LambdaFunctionUrlClient extends Server {
42+
private lambdaConfig: Required<LambdaFunctionUrlConfig>;
43+
44+
constructor(name: string, config: LambdaFunctionUrlConfig) {
45+
// Set defaults and convert to required config
46+
const fullConfig: Required<LambdaFunctionUrlConfig> = {
47+
functionUrl: config.functionUrl || "",
48+
stackName: config.stackName || "",
49+
stackUrlOutputKey: config.stackUrlOutputKey || "FunctionUrl",
50+
region: config.region || "us-east-2",
51+
};
52+
53+
super(name, fullConfig);
54+
this.lambdaConfig = fullConfig;
55+
56+
if (!fullConfig.functionUrl && !fullConfig.stackName) {
57+
throw new Error(
58+
"Either functionUrl must be provided or stackName must be provided for CloudFormation lookup"
59+
);
60+
}
61+
62+
if (fullConfig.functionUrl && fullConfig.stackName) {
63+
throw new Error("Only one of functionUrl or stackName can be provided");
64+
}
65+
}
66+
67+
/**
68+
* Initialize the server connection with AWS SigV4 authentication.
69+
* @throws Error if initialization parameters are invalid
70+
* @throws Error if server fails to initialize
71+
*/
72+
async initialize(): Promise<void> {
73+
try {
74+
// Determine the function URL
75+
let functionUrl = this.lambdaConfig.functionUrl;
76+
if (this.lambdaConfig.stackName) {
77+
logger.debug("Retrieving function URL from CloudFormation...");
78+
functionUrl = await this._getFunctionUrlFromCloudFormation();
79+
// Update the config with the resolved URL
80+
this.lambdaConfig.functionUrl = functionUrl;
81+
}
82+
83+
if (!functionUrl) {
84+
throw new Error(
85+
"The functionUrl must be a valid string and cannot be undefined."
86+
);
87+
}
88+
89+
logger.debug(`Connecting to Lambda function URL: ${functionUrl}`);
90+
91+
const transport = new StreamableHTTPClientWithSigV4Transport(
92+
new URL(functionUrl),
93+
{
94+
service: "lambda",
95+
region: this.lambdaConfig.region,
96+
}
97+
);
98+
99+
await this.client.connect(transport);
100+
logger.debug("MCP session initialized successfully");
101+
} catch (error) {
102+
logger.error(
103+
`Error initializing Lambda function URL client ${this.name}: ${error}`
104+
);
105+
throw error;
106+
}
107+
}
108+
109+
/**
110+
* Retrieve the Lambda function URL from CloudFormation stack outputs.
111+
*/
112+
private async _getFunctionUrlFromCloudFormation(): Promise<string> {
113+
try {
114+
logger.debug(
115+
`Retrieving function URL from CloudFormation stack: ${this.lambdaConfig.stackName}`
116+
);
117+
118+
const cfClient = new CloudFormationClient({
119+
region: this.lambdaConfig.region,
120+
});
121+
122+
const command = new DescribeStacksCommand({
123+
StackName: this.lambdaConfig.stackName,
124+
});
125+
126+
const response = await cfClient.send(command);
127+
128+
if (!response.Stacks || response.Stacks.length === 0) {
129+
throw new Error(
130+
`CloudFormation stack '${this.lambdaConfig.stackName}' not found`
131+
);
132+
}
133+
134+
const stack = response.Stacks[0];
135+
if (!stack.Outputs) {
136+
throw new Error(
137+
`No outputs found in CloudFormation stack '${this.lambdaConfig.stackName}'`
138+
);
139+
}
140+
141+
const functionUrlOutput = stack.Outputs.find(
142+
(output) => output.OutputKey === this.lambdaConfig.stackUrlOutputKey
143+
);
144+
145+
if (!functionUrlOutput || !functionUrlOutput.OutputValue) {
146+
throw new Error(
147+
`Function URL output not found in CloudFormation stack. Output key: ${this.lambdaConfig.stackUrlOutputKey}`
148+
);
149+
}
150+
151+
const functionUrl = functionUrlOutput.OutputValue;
152+
logger.debug(`Retrieved function URL: ${functionUrl}`);
153+
return functionUrl;
154+
} catch (error: any) {
155+
if (error.name === "ValidationException") {
156+
throw new Error(
157+
`CloudFormation stack '${this.lambdaConfig.stackName}' does not exist or is not accessible`
158+
);
159+
} else if (
160+
error.name === "AccessDenied" ||
161+
error.name === "UnauthorizedOperation"
162+
) {
163+
throw new Error(
164+
`Insufficient permissions to access CloudFormation stack '${this.lambdaConfig.stackName}'. ` +
165+
"Ensure your AWS credentials have cloudformation:DescribeStacks permission."
166+
);
167+
} else {
168+
logger.error(
169+
"Failed to retrieve function URL from CloudFormation:",
170+
error
171+
);
172+
throw new Error(
173+
`Could not retrieve function URL from CloudFormation stack ${this.lambdaConfig.stackName}: ${error.message}`
174+
);
175+
}
176+
}
177+
}
178+
}

examples/chatbots/typescript/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/chatbots/typescript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@aws-sdk/client-bedrock-runtime": "^3.842.0",
1313
"@aws-sdk/client-cloudformation": "^3.839.0",
14+
"@aws-sdk/credential-provider-node": "^3.840.0",
1415
"@modelcontextprotocol/sdk": "^1.15.0",
1516
"readline-sync": "^1.4.10",
1617
"winston": "^3.17.0",

examples/chatbots/typescript/servers_config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
"region": "us-east-2"
1616
}
1717
},
18+
"lambdaFunctionUrls": {
19+
"mcpdoc": {
20+
"stackName": "LambdaMcpServer-Mcpdoc"
21+
},
22+
"cat-facts": {
23+
"stackName": "LambdaMcpServer-CatFacts"
24+
}
25+
},
1826
"oAuthServers": {
1927
"dog-facts": {
2028
"serverStackName": "LambdaMcpServer-DogFacts"

examples/chatbots/typescript/src/main.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { ChatSession } from "./chat_session.js";
33
import { LLMClient } from "./llm_client.js";
44
import { StdioServer } from "./server_clients/stdio_server.js";
55
import { LambdaFunctionClient } from "./server_clients/lambda_function.js";
6+
import {
7+
LambdaFunctionUrlClient,
8+
LambdaFunctionUrlConfig,
9+
} from "./server_clients/lambda_function_url.js";
610
import {
711
InteractiveOAuthClient,
812
InteractiveOAuthConfig,
@@ -33,6 +37,15 @@ async function main(): Promise<void> {
3337
);
3438
}
3539

40+
// Initialize Lambda function URL servers
41+
for (const [name, srvConfig] of Object.entries(
42+
serverConfig.lambdaFunctionUrls || {}
43+
)) {
44+
servers.push(
45+
new LambdaFunctionUrlClient(name, srvConfig as LambdaFunctionUrlConfig)
46+
);
47+
}
48+
3649
// Initialize interactive OAuth servers
3750
for (const [name, srvConfig] of Object.entries(
3851
serverConfig.oAuthServers || {}

0 commit comments

Comments
 (0)