Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"group": "Hosted",
"pages": ["hosted/cloud", "hosted/docker", "hosted/cloudflare"]
},
{
"group": "Integrations",
"pages": ["integrations/aws-mcp"]
},
{
"group": "Concepts",
"pages": ["concepts/integrations", "concepts/connections", "concepts/policies"]
Expand Down
114 changes: 114 additions & 0 deletions apps/docs/integrations/aws-mcp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: AWS MCP
description: "Connect Executor to the managed AWS MCP Server with an IAM role and short-lived OAuth tokens."
---

Executor connects directly to the managed AWS MCP Server over Streamable HTTP. An Executor
connection stores bootstrap AWS credentials and a target role ARN. At runtime Executor:

1. assumes the target role with AWS STS;
2. verifies the resulting account and role identity;
3. exchanges the temporary role credentials for a short-lived AWS MCP bearer token; and
4. uses that bearer through Executor's normal MCP discovery and execution path.

Temporary role credentials and bearer tokens are cached in memory until shortly before expiry.
They are never written to the connection store.

## Create the bootstrap identity

Create an IAM user dedicated to Executor and generate an access key. Its only permission should be
assuming the target role:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::123456789012:role/ExecutorAwsMcp"
}
]
}
```

For two AWS accounts, add one target role ARN per account to `Resource`, or use a separate
bootstrap identity for each account.

## Create the target role

Create `ExecutorAwsMcp` in the account Executor should access. Its trust policy must name the
bootstrap identity. Add an external ID condition if you use one in the Executor account form.
Remove the `Condition` block from this example when you do not use an external ID.

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:user/executor-aws-mcp"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "replace-with-a-random-value"
}
}
}
]
}
```

The target role needs the AWS permissions its MCP tools may use, plus permission to mint the AWS
MCP bearer:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "signin:CreateOAuth2Token",
"Resource": "arn:aws:signin:us-east-1:123456789012:service-principal/aws-mcp.amazonaws.com",
"Condition": {
"StringEquals": {
"signin:OAuthClientId": "arn:aws:signin:::client-credentials/sigv4",
"signin:OAuthGrantType": "client_credentials",
"aws:RequestedRegion": "us-east-1"
}
}
}
]
}
```

Attach your AWS service permissions separately. The OAuth token does not grant permissions the
role does not already have. A broad managed policy can be useful for an initial private proof, but
the target role is the security boundary for generic AWS MCP tools such as `call_aws` and
`run_script`.

## Add AWS MCP in Executor

1. Select **Add Integration**, choose **AWS MCP**, and add the managed server.
2. On the AWS MCP integration page, select **Add Account**.
3. Choose the normal Executor owner and account name.
4. Enter the bootstrap access key ID and secret, target role ARN, and optional session token and
external ID. Executor derives the authentication region from the managed endpoint.
5. Run the account check. A healthy result includes the AWS account ID and assumed-role ARN.

Repeat **Add Account** for another AWS role or account. Each connection gets its own native tool
address, search entries, health status, credential provider, and policies.

Executor includes managed presets for the AWS MCP endpoints in `us-east-1` and `eu-central-1`.
Choose the endpoint whose region should handle the MCP connection; its region must also match the
`signin:CreateOAuth2Token` resource and `aws:RequestedRegion` condition in the target-role policy.

<Note>
AWS IAM authentication is accepted only for the managed AWS MCP endpoints. Executor will not
forward an IAM-derived bearer to a custom MCP URL.
</Note>

For the AWS-side behavior and available regions, see the
[AWS MCP authentication guide](https://docs.aws.amazon.com/signin/latest/userguide/aws-mcp-server.html).
20 changes: 20 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export {
// Integration / connection / tool domain contracts.
export type {
AuthMethodDescriptor,
AuthMethodCredentialInputDescriptor,
AuthMethodOAuthDescriptor,
AuthPlacementDescriptor,
Integration,
Expand Down
20 changes: 20 additions & 0 deletions packages/core/sdk/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ export interface AuthMethodOAuthDescriptor {
readonly supportsClientIdMetadataDocument?: boolean;
}

/** One value collected when an authentication strategy needs credential
* material that is not itself an HTTP placement. Provider-backed strategies
* such as AWS IAM use these inputs to derive the request credential at runtime
* (for example, exchanging an assumed role for a short-lived bearer token). */
export interface AuthMethodCredentialInputDescriptor {
/** Stable key used in the connection's encrypted values map. */
readonly variable: string;
readonly label: string;
readonly description?: string;
readonly placeholder?: string;
/** Secret inputs are masked in the account form. Defaults to true. */
readonly secret?: boolean;
/** Optional inputs may be omitted when creating or validating a connection. */
readonly optional?: boolean;
}

/** A single declared auth method on an integration's catalog response. */
export interface AuthMethodDescriptor {
/** Stable id within the integration (e.g. the auth template slug). */
Expand All @@ -75,6 +91,10 @@ export interface AuthMethodDescriptor {
/** The auth-template slug a connection binds against. */
readonly template: string;
readonly placements?: readonly AuthPlacementDescriptor[];
/** Named credential values consumed by the owning plugin rather than
* rendered directly onto an HTTP request. Mutually exclusive with
* placement-derived inputs for built-in methods. */
readonly credentialInputs?: readonly AuthMethodCredentialInputDescriptor[];
readonly oauth?: AuthMethodOAuthDescriptor;
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/sdk/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { connectionIdentifier, isConnectionIdentifier } from "./connection-name-
// Domain projections (types only — no runtime cost).
export type {
AuthMethodDescriptor,
AuthMethodCredentialInputDescriptor,
AuthMethodOAuthDescriptor,
AuthPlacementDescriptor,
Integration,
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
"@executor-js/config": "workspace:*",
"@executor-js/sdk": "workspace:*",
"@modelcontextprotocol/sdk": "^1.29.0",
"aws4fetch": "1.0.20",
"fast-xml-parser": "5.10.1",
"zod": "4.3.6"
},
"devDependencies": {
Expand Down
35 changes: 28 additions & 7 deletions packages/plugins/mcp/src/react/AddMcpIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,18 @@ export default function AddMcpIntegration(props: {
return [{ value: { kind: "none" }, label: "Detected" }];
}, [probe]);
const authMethodList = useAuthMethodList(authMethodSeeds);
const presetAuthenticationTemplate =
preset && preset.transport === undefined && state.url.trim() === preset.endpoint
? preset.authenticationTemplate
: undefined;

const remoteIdentity = useIntegrationIdentity({
fallbackName:
integrationDisplayNameFromUrl(state.url, "MCP") ?? probe?.serverName ?? probe?.name ?? "",
(!isStdioPreset && preset?.transport === undefined ? preset.name : undefined) ??
integrationDisplayNameFromUrl(state.url, "MCP") ??
probe?.serverName ??
probe?.name ??
"",
});
// Agent-visible description: prefilled from the server's `instructions`
// until the user types (null = untouched, keep deriving from the probe).
Expand Down Expand Up @@ -337,15 +345,17 @@ export default function AddMcpIntegration(props: {
dispatch({ type: "add-start" });
// Every row registers as a declared method (a lone no-auth row registers
// the open-server method). Slugs are assigned server-side by kind.
const methods = authMethodList.rows.map((row: AuthMethodRow) =>
mcpWireAuthInput(mcpAuthMethodInputFromEditorValue(row.value)),
);
const methods = presetAuthenticationTemplate
? [...presetAuthenticationTemplate]
: authMethodList.rows.map((row: AuthMethodRow) =>
mcpWireAuthInput(mcpAuthMethodInputFromEditorValue(row.value)),
);
const slug = await registerIntegration(
methods.length > 0 ? methods : [{ kind: "none" as const }],
);
if (slug === null) return;
props.onComplete(slug);
}, [probe, authMethodList.rows, registerIntegration, props]);
}, [probe, presetAuthenticationTemplate, authMethodList.rows, registerIntegration, props]);

// ---- Stdio actions ----

Expand Down Expand Up @@ -434,15 +444,26 @@ export default function AddMcpIntegration(props: {
shared list editor. The credentials themselves (API key value /
OAuth sign-in) are added from the integration's detail hub after
adding. */}
{probe && (
{probe && presetAuthenticationTemplate ? (
<CardStack>
<CardStackContent className="border-t-0">
<CardStackEntryField
label="Authentication"
description="- AWS IAM role credentials are configured when you add an account."
>
<p className="text-sm font-medium">AWS IAM role</p>
</CardStackEntryField>
</CardStackContent>
</CardStack>
) : probe ? (
<AuthMethodListEditor
list={authMethodList}
title="How does this server authenticate?"
oauthMetadata="discovered"
emptyHint="No methods declared. Add a method, or add the server without auth and connect from the integration page later."
footerHint="Every method here is registered with the server. Connect an account from the integration page after adding."
/>
)}
) : null}

{/* Error (add server). Probe errors show inline on the field. */}
{otherError && (
Expand Down
24 changes: 24 additions & 0 deletions packages/plugins/mcp/src/react/EditMcpIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type McpRemoteConfig = Extract<McpIntegrationConfig, { transport: "remote" }>;
const methodSeedLabel = (method: McpAuthMethod): string => {
if (method.kind === "oauth2") return "OAuth";
if (method.kind === "apikey") return apiKeyMethodLabel(method);
if (method.kind === "aws_iam") return "AWS IAM role";
return "No authentication";
};

Expand Down Expand Up @@ -202,6 +203,25 @@ function StdioReadOnly(props: {
);
}

function AwsIamReadOnly() {
return (
<div className="space-y-3 border-t border-border/60 pt-5">
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">Authentication method</p>
<p className="text-xs text-muted-foreground">
AWS IAM credentials and the target role are configured independently on each account.
</p>
</div>
<div className="flex items-center gap-3 rounded-md border border-border/60 bg-muted/40 px-3 py-2">
<p className="min-w-0 flex-1 truncate font-mono text-xs text-foreground">AWS IAM role</p>
<Badge variant="secondary" className="text-xs">
managed
</Badge>
</div>
</div>
);
}

// ---------------------------------------------------------------------------
// Main component — the mcp plugin's section of the integration Edit sheet.
// `integrationId` is the integration slug (v2).
Expand All @@ -227,6 +247,10 @@ export default function EditMcpIntegration({
);
}

if (server.config.authenticationTemplate.some((method) => method.kind === "aws_iam")) {
return <AwsIamReadOnly />;
}

return (
<RemoteEdit
server={server as McpServer & { config: McpRemoteConfig }}
Expand Down
Loading