Skip to content

Commit 3f21a1a

Browse files
committed
feat: Migrate Typescript e2e tests to Strands Agents TS SDK
1 parent f520a00 commit 3f21a1a

File tree

15 files changed

+2222
-2411
lines changed

15 files changed

+2222
-2411
lines changed

e2e_tests/typescript/package-lock.json

Lines changed: 1931 additions & 982 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e_tests/typescript/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "mcp-e2e-test-typescript",
33
"version": "0.5.3",
4+
"type": "module",
45
"main": "build/main.js",
56
"types": "build/main.d.ts",
67
"scripts": {
@@ -13,10 +14,12 @@
1314
"@aws-sdk/client-cloudformation": "^3.922.0",
1415
"@aws-sdk/client-secrets-manager": "^3.922.0",
1516
"@aws-sdk/client-ssm": "^3.922.0",
16-
"@modelcontextprotocol/sdk": "^1.20.2",
17+
"@modelcontextprotocol/sdk": "^1.24.2",
18+
"@strands-agents/sdk": "^0.1.1",
1719
"winston": "^3.18.3"
1820
},
1921
"devDependencies": {
22+
"@cfworker/json-schema": "^4.1.1",
2023
"@eslint/js": "^9.39.0",
2124
"@types/node": "^24.10.0",
2225
"eslint": "^9.39.0",
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { URL } from "node:url";
2+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3+
import {
4+
OAuthClientInformation,
5+
OAuthClientInformationFull,
6+
OAuthClientMetadata,
7+
OAuthTokens,
8+
} from "@modelcontextprotocol/sdk/shared/auth.js";
9+
import {
10+
OAuthClientProvider,
11+
discoverAuthorizationServerMetadata,
12+
discoverOAuthProtectedResourceMetadata,
13+
extractResourceMetadataUrl,
14+
} from "@modelcontextprotocol/sdk/client/auth.js";
15+
16+
export async function createAutomatedOAuthTransport(
17+
serverUrl: string,
18+
clientId: string,
19+
clientSecret: string
20+
): Promise<StreamableHTTPClientTransport> {
21+
const scope = await discoverScope(serverUrl);
22+
23+
const clientMetadata: OAuthClientMetadata = {
24+
client_name: "MCP Client",
25+
redirect_uris: [],
26+
grant_types: ["client_credentials"],
27+
response_types: [],
28+
token_endpoint_auth_method: "client_secret_basic",
29+
scope,
30+
};
31+
32+
const oauthProvider = new AutomatedOAuthClientProvider(clientMetadata, clientId, clientSecret);
33+
await performClientCredentialsFlow(serverUrl, oauthProvider);
34+
35+
return new StreamableHTTPClientTransport(new URL(serverUrl), {
36+
authProvider: oauthProvider,
37+
});
38+
}
39+
40+
async function discoverScope(serverUrl: string): Promise<string> {
41+
const response = await fetch(serverUrl, {
42+
method: "POST",
43+
headers: { "Content-Type": "application/json" },
44+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
45+
});
46+
const resourceMetadataUrl = extractResourceMetadataUrl(response);
47+
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
48+
return resourceMetadata.scopes_supported?.join(" ") || "";
49+
}
50+
51+
async function performClientCredentialsFlow(serverUrl: string, oauthProvider: AutomatedOAuthClientProvider): Promise<void> {
52+
if (oauthProvider.tokens()?.access_token) return;
53+
54+
const response = await fetch(serverUrl, {
55+
method: "POST",
56+
headers: { "Content-Type": "application/json" },
57+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
58+
});
59+
const resourceMetadataUrl = extractResourceMetadataUrl(response);
60+
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
61+
const authServerUrl = resourceMetadata.authorization_servers?.[0];
62+
if (!authServerUrl) throw new Error("No authorization server found");
63+
64+
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
65+
if (!metadata?.token_endpoint) throw new Error("No token endpoint found");
66+
67+
const clientInfo = oauthProvider.clientInformation();
68+
if (!clientInfo) throw new Error("No client information available");
69+
70+
const params = new URLSearchParams({
71+
grant_type: "client_credentials",
72+
client_id: clientInfo.client_id,
73+
client_secret: clientInfo.client_secret!,
74+
scope: oauthProvider.clientMetadata.scope || "",
75+
});
76+
77+
const tokenResponse = await fetch(metadata.token_endpoint, {
78+
method: "POST",
79+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
80+
body: params,
81+
});
82+
83+
if (!tokenResponse.ok) throw new Error(`Token request failed: ${tokenResponse.status}`);
84+
oauthProvider.saveTokens(await tokenResponse.json());
85+
}
86+
87+
class AutomatedOAuthClientProvider implements OAuthClientProvider {
88+
private _clientInformation: OAuthClientInformationFull;
89+
private _tokens?: OAuthTokens;
90+
91+
constructor(
92+
private readonly _clientMetadata: OAuthClientMetadata,
93+
clientId: string,
94+
clientSecret: string
95+
) {
96+
this._clientInformation = {
97+
client_id: clientId,
98+
client_secret: clientSecret,
99+
...this._clientMetadata,
100+
};
101+
}
102+
103+
get redirectUrl(): string | URL {
104+
return "";
105+
}
106+
107+
get clientMetadata(): OAuthClientMetadata {
108+
return this._clientMetadata;
109+
}
110+
111+
clientInformation(): OAuthClientInformation | undefined {
112+
return this._clientInformation;
113+
}
114+
115+
saveClientInformation(clientInformation: OAuthClientInformationFull): void {
116+
this._clientInformation = clientInformation;
117+
}
118+
119+
tokens(): OAuthTokens | undefined {
120+
return this._tokens;
121+
}
122+
123+
saveTokens(tokens: OAuthTokens): void {
124+
this._tokens = tokens;
125+
}
126+
127+
redirectToAuthorization(): void {
128+
throw new Error("redirectToAuthorization should not be called in automated OAuth flow");
129+
}
130+
131+
saveCodeVerifier(): void {
132+
throw new Error("saveCodeVerifier should not be called in automated OAuth flow");
133+
}
134+
135+
codeVerifier(): string {
136+
throw new Error("codeVerifier should not be called in automated OAuth flow");
137+
}
138+
}
Lines changed: 12 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,26 @@
1-
import { Server } from "./server_clients/server.js";
2-
import { Servers } from "./server_clients/servers.js";
3-
import { LLMClient } from "./llm_client.js";
1+
import { Agent, McpClient } from "@strands-agents/sdk";
42
import logger from "./logger.js";
5-
import {
6-
ContentBlock,
7-
ConverseCommandOutput,
8-
Message,
9-
} from "@aws-sdk/client-bedrock-runtime";
103

11-
/**
12-
* Orchestrates the interaction between user, LLM, and tools.
13-
*/
144
export class ChatSession {
15-
private servers: Server[];
16-
private llmClient: LLMClient;
17-
private userUtterances: string[] = [];
5+
private agent: Agent;
6+
private userUtterances: string[];
7+
private mcpClients: McpClient[];
188

19-
constructor(
20-
servers: Server[],
21-
llmClient: LLMClient,
22-
userUtterances: string[]
23-
) {
24-
this.servers = servers;
25-
this.llmClient = llmClient;
9+
constructor(agent: Agent, userUtterances: string[], mcpClients: McpClient[]) {
10+
this.agent = agent;
2611
this.userUtterances = userUtterances;
12+
this.mcpClients = mcpClients;
2713
}
2814

29-
/**
30-
* Process the LLM response and execute tools if needed.
31-
* @param serversManager The servers manager.
32-
* @param llmResponse The response from the Bedrock Converse API.
33-
* @returns The result of tool execution, if any.
34-
*/
35-
async executeRequestedTools(
36-
serversManager: Servers,
37-
llmResponse: ConverseCommandOutput
38-
): Promise<Message | null> {
39-
const stopReason = llmResponse.stopReason;
40-
41-
if (stopReason === "tool_use") {
42-
try {
43-
const toolResponses: ContentBlock[] = [];
44-
for (const contentItem of llmResponse.output!.message!.content!) {
45-
if ("toolUse" in contentItem) {
46-
logger.info(`Executing tool: ${contentItem.toolUse!.name}`);
47-
logger.info(
48-
`With arguments: ${JSON.stringify(contentItem.toolUse!.input)}`
49-
);
50-
const response = await serversManager.executeTool(
51-
contentItem.toolUse!.name!,
52-
contentItem.toolUse!.toolUseId!,
53-
contentItem.toolUse!.input! as Record<string, any>
54-
);
55-
toolResponses.push(response);
56-
}
57-
}
58-
return { role: "user", content: toolResponses };
59-
} catch (e) {
60-
throw new Error(`Failed to execute tool: ${e}`);
61-
}
62-
} else {
63-
// Assume this catches stop reasons "end_turn", "stop_sequence", and "max_tokens"
64-
return null;
65-
}
66-
}
67-
68-
/**
69-
* Main chat session handler.
70-
*/
7115
async start(): Promise<void> {
72-
const serverManager = new Servers(this.servers);
73-
7416
try {
75-
await serverManager.initialize();
76-
77-
const allTools = await serverManager.listTools();
78-
const toolsDescription = allTools.map((tool) => tool.formatForLLM());
79-
80-
const systemPrompt = "You are a helpful assistant.";
81-
82-
const messages: Message[] = [];
83-
84-
for (const [i, userInput] of this.userUtterances.entries()) {
85-
// To reduce token use, clear out past messages for each utterance
86-
messages.length = 0;
87-
88-
console.log(`\You: ${userInput}`);
89-
90-
messages.push({ role: "user", content: [{ text: userInput }] });
91-
92-
let llmResponse: ConverseCommandOutput | null = null;
93-
while (llmResponse === null || llmResponse.stopReason === "tool_use") {
94-
// Get the next response
95-
llmResponse = await this.llmClient.getResponse(
96-
messages,
97-
systemPrompt,
98-
toolsDescription
99-
);
100-
messages.push(llmResponse.output!.message!);
101-
102-
logger.debug("\nAssistant: " + JSON.stringify(llmResponse, null, 2));
103-
if (llmResponse.output?.message?.content?.[0].text) {
104-
console.log(
105-
`\nAssistant: ${llmResponse.output!.message!.content![0].text}`
106-
);
107-
}
108-
109-
// Execute tools if requested
110-
const toolResults = await this.executeRequestedTools(
111-
serverManager,
112-
llmResponse
113-
);
114-
115-
if (toolResults) {
116-
logger.debug(
117-
"\nTool Results: " + JSON.stringify(toolResults, null, 2)
118-
);
119-
messages.push(toolResults);
120-
}
121-
}
17+
for (const userInput of this.userUtterances) {
18+
console.log(`\nYou: ${userInput}`);
19+
process.stdout.write("\nAssistant: ");
20+
await this.agent.invoke(userInput);
12221
}
12322
} finally {
124-
await serverManager.close();
23+
await Promise.all(this.mcpClients.map(client => client.disconnect()));
12524
}
12625
}
12726
}

e2e_tests/typescript/src/llm_client.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)