Skip to content
Merged
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
2 changes: 2 additions & 0 deletions infra/packages/shared/src/gateway_priorities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum GatewayService {
CONNECTION_GATEWAY = 'CONNECTION_GATEWAY',
AUTHENTICATION_SERVICE = 'AUTHENTICATION_SERVICE',
EMAIL_SERVICE = 'EMAIL_SERVICE',
MCP_SERVER = 'MCP_SERVER',
DOCUMENT_COGNITION_SERVICE = 'DOCUMENT_COGNITION_SERVICE',
}

Expand All @@ -39,6 +40,7 @@ export const GATEWAY_PRIORITIES: GatewayPriorityMap = {
[GatewayService.CONNECTION_GATEWAY]: 90,
[GatewayService.AUTHENTICATION_SERVICE]: 100,
[GatewayService.EMAIL_SERVICE]: 110,
[GatewayService.MCP_SERVER]: 130,
[GatewayService.DOCUMENT_COGNITION_SERVICE]: 120,
[GatewayService.CONVERT_SERVICE]: 3000,
};
Expand Down
3 changes: 3 additions & 0 deletions infra/packages/shared/src/service_urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum ServiceUrl {
LEXICAL_SERVICE_URL = 'LEXICAL_SERVICE_URL',
UNFURL_SERVICE_URL = 'UNFURL_SERVICE_URL',
AGENT_HARNESS_SERVICE_URL = 'AGENT_HARNESS_SERVICE_URL',
MCP_SERVER_URL = 'MCP_SERVER_URL',
}

/**
Expand Down Expand Up @@ -47,6 +48,7 @@ const DEV_SERVICE_URLS: ServiceUrlMap = {
[ServiceUrl.UNFURL_SERVICE_URL]: 'https://dev-gateway.macro.com/unfurl',
[ServiceUrl.AGENT_HARNESS_SERVICE_URL]:
'https://dev-gateway.macro.com/agent-harness',
[ServiceUrl.MCP_SERVER_URL]: 'https://dev-gateway.macro.com/mcp',
};

/**
Expand All @@ -70,6 +72,7 @@ const PROD_SERVICE_URLS: ServiceUrlMap = {
[ServiceUrl.UNFURL_SERVICE_URL]: 'https://gateway.macro.com/unfurl',
[ServiceUrl.AGENT_HARNESS_SERVICE_URL]:
'https://gateway.macro.com/agent-harness',
[ServiceUrl.MCP_SERVER_URL]: 'https://gateway.macro.com/mcp',
};

/**
Expand Down
3 changes: 3 additions & 0 deletions infra/stacks/fusionauth-instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ const macroApplication = new FusionAuthApplication(
]
: []),
`https://mcp-server${stack === 'prod' ? '' : `-${stack}`}.macro.com/oauth/callback`,
...(stack === 'dev' || stack === 'prod'
? [`${getServiceUrl(ServiceUrl.MCP_SERVER_URL)}/oauth/callback`]
: []),
...(stack === 'local' || stack === 'dev'
? ['http://localhost:8085/*', 'http://localhost:8085/oauth/*']
: []),
Expand Down
37 changes: 37 additions & 0 deletions infra/stacks/mcp-server/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ import {
datadogAgentContainer,
fargateLogRouterSidecarContainer,
serviceLoadBalancer,
ServiceTargetGroup,
} from '../../packages/resources';
import { EcrImage } from '../../packages/service';
import {
BASE_DOMAIN,
CLOUD_TRAIL_SNS_TOPIC_ARN,
DopplerEcsEnvironment,
getGatewayAlb,
getKafkaClusterPolicy,
GatewayService,
stack,
} from '../../packages/shared';

const gatewayLoadBalancer = getGatewayAlb();

const BASE_NAME = pulumi.getProject();
const REPO_ROOT = '../../..';

Expand Down Expand Up @@ -107,6 +112,22 @@ export class McpServer extends pulumi.ComponentResource {
this.serviceAlbSg = sg.serviceAlbSg;
this.serviceSg = sg.serviceSg;

const gatewayTargetGroup = new ServiceTargetGroup(
`${stack}-${BASE_NAME}`,
{
tags: this.tags,
listenerArn: gatewayLoadBalancer.httpsListenerArn,
vpcId: vpc.vpcId,
containerPort: serviceContainerPort,
service: GatewayService.MCP_SERVER,
healthCheckPath,
pathPatterns: ['/mcp', '/mcp/*'],
serviceSecurityGroupId: this.serviceSg.id,
albSecurityGroupId: gatewayLoadBalancer.albSecurityGroupId,
},
{ parent: this }
);

// lb
const { targetGroup, lb, listener } = serviceLoadBalancer(this, {
serviceName: BASE_NAME,
Expand Down Expand Up @@ -245,6 +266,18 @@ export class McpServer extends pulumi.ComponentResource {
enable: true,
rollback: true,
},
loadBalancers: [
{
targetGroupArn: targetGroup.arn,
containerName: 'service',
containerPort: serviceContainerPort,
},
{
targetGroupArn: gatewayTargetGroup.target_group.arn,
containerName: 'service',
containerPort: serviceContainerPort,
},
],
Comment thread
whutchinson98 marked this conversation as resolved.
taskDefinitionArgs: {
taskRole: {
roleArn: this.role.arn,
Expand Down Expand Up @@ -299,6 +332,10 @@ export class McpServer extends pulumi.ComponentResource {
},
{
parent: this,
// ECS refuses a service whose target group is not yet associated with
// a load balancer. The dedicated HTTPS listener and the gateway
// listener rule each create that association for their target group.
dependsOn: [listener, gatewayTargetGroup.listener_rule],
}
);

Expand Down
1 change: 1 addition & 0 deletions services/mcp_auth_proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ uuid = { workspace = true }
workspace-hack = { version = "0.1", path = "../../crates/workspace-hack" }

[dev-dependencies]
macro_auth = { path = "../../crates/macro_auth", features = ["testing"] }
tokio = { workspace = true }
15 changes: 15 additions & 0 deletions services/mcp_auth_proxy/src/domain/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ pub trait InflightAuthStore: Send + Sync {
fn cleanup_expired(&self) -> impl Future<Output = anyhow::Result<()>> + Send;
}

/// Streamable HTTP resource URL advertised in protected-resource metadata.
///
/// MCP clients require a `resource` field. When `MCP_PUBLIC_URL` is an origin
/// (the current Doppler value), append `/mcp`. When it already ends in `/mcp`
/// (a later gateway cutover), do not append again.
fn mcp_resource_url(public_url: &str) -> String {
let base = public_url.trim_end_matches('/');
if base.ends_with("/mcp") {
base.to_owned()
} else {
format!("{base}/mcp")
}
}

/// Domain service backing the MCP OAuth broker.
pub struct McpAuthProxyServiceImpl<I> {
inflight_auth: Arc<I>,
Expand Down Expand Up @@ -205,6 +219,7 @@ where
tracing::debug!("oauth-protected-resource metadata requested");
let base = &self.public_url;
serde_json::json!({
"resource": mcp_resource_url(base),
"authorization_server": base,
"authorization_servers": [base],
})
Expand Down
32 changes: 32 additions & 0 deletions services/mcp_auth_proxy/src/domain/service/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,38 @@ fn service(store: FakeInflightAuth) -> McpAuthProxyServiceImpl<FakeInflightAuth>
)
}

#[test]
fn protected_resource_metadata_includes_required_resource_url() {
let json = service(FakeInflightAuth::default()).protected_resource_metadata();
assert_eq!(
json,
serde_json::json!({
"resource": "https://mcp.example.com/mcp",
"authorization_server": "https://mcp.example.com",
"authorization_servers": ["https://mcp.example.com"],
})
);
}

#[test]
fn protected_resource_metadata_does_not_double_the_mcp_path() {
let service = McpAuthProxyServiceImpl::new(
"https://gateway.macro.com/mcp".to_owned(),
Arc::new(FakeInflightAuth::default()),
Arc::new(FakeOAuthProvider {
expires_in: UPSTREAM_EXPIRES_IN,
}),
);
assert_eq!(
service.protected_resource_metadata(),
serde_json::json!({
"resource": "https://gateway.macro.com/mcp",
"authorization_server": "https://gateway.macro.com/mcp",
"authorization_servers": ["https://gateway.macro.com/mcp"],
})
);
}

fn issued_code(access_token_expires_at: Option<SystemTime>) -> IssuedAuthorizationCode {
IssuedAuthorizationCode {
access_token: AccessToken::from("upstream-access"),
Expand Down
3 changes: 3 additions & 0 deletions services/mcp_auth_proxy/src/inbound.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Inbound adapters for the MCP OAuth broker.

/// Path the shared gateway forwards without stripping.
pub(crate) const GATEWAY_PATH_PREFIX: &str = "/mcp";

/// Axum router for the MCP OAuth broker.
pub mod axum_router;
/// Bearer token middleware for the protected MCP endpoint.
Expand Down
19 changes: 10 additions & 9 deletions services/mcp_auth_proxy/src/inbound/axum_router.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Axum router for the MCP OAuth broker.

#[cfg(test)]
mod test;

use std::time::Duration;

use axum::{
Expand Down Expand Up @@ -217,10 +220,6 @@ where
"/.well-known/oauth-protected-resource/mcp",
routing::get(protected_resource_metadata),
)
.route(
"/mcp/.well-known/oauth-protected-resource",
routing::get(protected_resource_metadata),
)
.route(
"/.well-known/oauth-authorization-server",
routing::get(authorization_server_metadata),
Expand All @@ -229,10 +228,6 @@ where
"/.well-known/oauth-authorization-server/mcp",
routing::get(authorization_server_metadata),
)
.route(
"/mcp/.well-known/oauth-authorization-server",
routing::get(authorization_server_metadata),
)
.route("/authorize", routing::get(authorize))
.route("/register", routing::post(register))
.route("/oauth/callback", routing::get(oauth_callback))
Expand All @@ -247,7 +242,13 @@ where
super::middleware::validate_bearer,
));

oauth_routes.merge(mcp_route).layer(mcp_cors_layer())
mount_at_root_and_prefix(oauth_routes.merge(mcp_route)).layer(mcp_cors_layer())
}

fn mount_at_root_and_prefix(inner: Router) -> Router {
Router::new()
.merge(inner.clone())
.nest(super::GATEWAY_PATH_PREFIX, inner)
}

/// CORS layer for the MCP router.
Expand Down
Loading
Loading