@allma/core-cdk
Version:
Core AWS CDK constructs for deploying the Allma serverless AI orchestration platform.
163 lines • 8.65 kB
JavaScript
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { PermanentStepError, TransientStepError } from '@allma/core-types';
import axios from 'axios';
const secretsManager = new SecretsManagerClient({});
async function getCredentials(authentication) {
if (authentication.type === 'NONE') {
return undefined;
}
const command = new GetSecretValueCommand({ SecretId: authentication.secretArn });
const response = await secretsManager.send(command);
if (!response.SecretString) {
throw new PermanentStepError(`Secret ${authentication.secretArn} does not contain a SecretString.`);
}
const secretString = response.SecretString;
// Try to parse as JSON. If it fails with a SyntaxError, assume it's a plaintext secret.
try {
const secretJson = JSON.parse(secretString);
if (typeof secretJson !== 'object' || secretJson === null) {
throw new PermanentStepError(`Secret ${authentication.secretArn} parsed as JSON, but it is not an object.`);
}
const credential = secretJson[authentication.secretJsonKey];
if (credential === undefined) {
throw new PermanentStepError(`Secret ${authentication.secretArn} is a valid JSON object but does not contain the key '${authentication.secretJsonKey}'. Available keys: ${Object.keys(secretJson).join(', ')}`);
}
if (typeof credential !== 'string') {
throw new PermanentStepError(`The value for key '${authentication.secretJsonKey}' in secret ${authentication.secretArn} is not a string.`);
}
return credential;
}
catch (e) {
if (e instanceof SyntaxError) {
// JSON.parse failed. This is an expected case for plaintext secrets.
// We return the whole string and ignore `secretJsonKey`.
return secretString;
}
// Re-throw other errors (e.g., our PermanentStepErrors from above)
throw e;
}
}
export async function discoverTools(connection) {
const credential = await getCredentials(connection.authentication);
const headers = {};
if (credential) {
if (connection.authentication.type === 'BEARER_TOKEN') {
headers['Authorization'] = `Bearer ${credential}`;
}
else if (connection.authentication.type === 'API_KEY') {
// This is a common pattern, but may need to be adjusted based on the MCP server's specific header requirements.
headers['X-API-Key'] = credential;
}
}
const payload = {
jsonrpc: '2.0',
method: 'tools/list',
id: '1',
};
try {
const response = await axios.post(connection.serverUrl, payload, { headers });
// Per JSON-RPC 2.0 spec, check for an error object in the response body.
if (response.data.error) {
const { code, message, data } = response.data.error;
throw new PermanentStepError(`MCP server returned a JSON-RPC error during tool discovery. Code: ${code}, Message: ${message}`, { errorData: data });
}
// Ensure the response data is a valid object before proceeding.
if (typeof response.data !== 'object' || response.data === null) {
throw new PermanentStepError(`MCP server returned a non-object response for tool discovery. Response body must be a JSON object.`, { receivedType: typeof response.data, responsePreview: String(response.data).substring(0, 500) });
}
// Ensure the result is an array as expected from the 'tools/list' method.
if (!Array.isArray(response.data.result)) {
throw new PermanentStepError(`MCP server returned an invalid response for tools/list. Expected 'result' to be an array.`, { receivedType: typeof response.data.result, resultPreview: JSON.stringify(response.data.result, null, 2).substring(0, 500) });
}
return response.data.result;
}
catch (error) {
// If we already threw a typed error, let it propagate.
if (error instanceof PermanentStepError || error instanceof TransientStepError) {
throw error;
}
if (axios.isAxiosError(error)) {
if (error.response) {
// The server responded with a non-2xx status code.
const { status, data } = error.response;
console.log(`[DEBUG] MCP server responded with error status ${status}. Raw response data:`, data);
if (status >= 500) {
// 5xx errors are server-side and may be temporary.
throw new TransientStepError(`MCP server returned a server error (HTTP ${status}) during tool discovery.`);
}
else if (status >= 400) {
// 4xx errors indicate a client-side problem (e.g., bad arguments) and are not recoverable.
throw new PermanentStepError(`MCP server returned a client error (HTTP ${status}) during tool discovery. Check connection settings.`, { responseData: data });
}
}
else if (error.request) {
// The request was made but no response was received (e.g., network error, DNS failure, timeout).
throw new TransientStepError('MCP server did not respond during tool discovery. This may be a temporary network issue.');
}
}
// For any other unexpected error, wrap it as a permanent failure to prevent incorrect retries.
// This includes JSON parsing errors from Axios itself if the server sends a malformed JSON response.
throw new PermanentStepError('An unexpected error occurred during tool discovery.', {
originalError: error instanceof Error ? { name: error.name, message: error.message } : error,
});
}
}
export async function callTool(connection, toolName, args) {
const credential = await getCredentials(connection.authentication);
const headers = {};
if (credential) {
if (connection.authentication.type === 'BEARER_TOKEN') {
headers['Authorization'] = `Bearer ${credential}`;
}
else if (connection.authentication.type === 'API_KEY') {
headers['X-API-Key'] = credential;
}
}
const payload = {
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: toolName,
arguments: args,
},
id: '1',
};
try {
const response = await axios.post(connection.serverUrl, payload, { headers });
// Per JSON-RPC 2.0 spec, the presence of an 'error' object indicates a failure, even with a 200 OK response.
if (response.data.error) {
const { code, message, data } = response.data.error;
throw new PermanentStepError(`MCP tool call failed with JSON-RPC error. Code: ${code}, Message: ${message}`, { errorData: data });
}
return response.data.result;
}
catch (error) {
// If we already threw a typed error, let it propagate.
if (error instanceof PermanentStepError || error instanceof TransientStepError) {
throw error;
}
if (axios.isAxiosError(error)) {
if (error.response) {
// The server responded with a non-2xx status code.
const { status, data } = error.response;
if (status >= 500) {
// 5xx errors are server-side and may be temporary.
throw new TransientStepError(`MCP server returned a server error (HTTP ${status}).`);
}
else if (status >= 400) {
// 4xx errors indicate a client-side problem (e.g., bad arguments) and are not recoverable.
throw new PermanentStepError(`MCP server returned a client error (HTTP ${status}). Please check the step configuration.`, { responseData: data });
}
}
else if (error.request) {
// The request was made but no response was received (e.g., network error, DNS failure, timeout).
throw new TransientStepError('MCP server did not respond. This may be a temporary network issue.');
}
}
// For any other unexpected error, wrap it as a permanent failure to prevent incorrect retries.
throw new PermanentStepError('An unexpected error occurred while calling the MCP tool.', {
originalError: error instanceof Error ? { name: error.name, message: error.message } : error,
});
}
}
//# sourceMappingURL=mcp-client.js.map