{
repo: R,
mcp_store: Arc,
+ mcp_connection: Arc,
creator: Arc,
recorder: Arc,
notifier: Option,
}
-impl Clone for ImportServiceImpl {
+impl Clone for ImportServiceImpl {
fn clone(&self) -> Self {
Self {
repo: self.repo.clone(),
mcp_store: self.mcp_store.clone(),
+ mcp_connection: self.mcp_connection.clone(),
creator: self.creator.clone(),
recorder: self.recorder.clone(),
notifier: self.notifier.clone(),
@@ -322,17 +324,19 @@ impl Clone for ImportServiceImpl {
}
}
-impl ImportServiceImpl {
+impl ImportServiceImpl {
/// Build the orchestrator.
pub fn new(
repo: R,
mcp_store: Arc,
+ mcp_connection: Arc,
creator: Arc,
recorder: Arc,
) -> Self {
Self {
repo,
mcp_store,
+ mcp_connection,
creator,
recorder,
notifier: None,
@@ -352,11 +356,12 @@ impl ImportServiceImpl {
}
}
-impl ImportServiceImpl
+impl ImportServiceImpl
where
R: ImportRepo + Clone,
S: McpServerStore,
C: EntityCreator,
+ P: McpConnection,
{
/// Spawn the gather session for one source; finishes the run row either
/// way and nudges the client.
@@ -719,18 +724,18 @@ where
user: &MacroUserIdStr<'static>,
source: ImportSource,
) -> anyhow::Result> {
- let url = source.mcp_server_url();
+ let app_slug = source.pipedream_app_slug();
let records: Vec = self
.mcp_store
.list(user)
.await
.map_err(|e| anyhow::anyhow!("mcp store: {e:?}"))?
.into_iter()
- .filter(|r| r.url == url)
+ .filter(|r| r.app_slug == app_slug)
.collect();
anyhow::ensure!(!records.is_empty(), "no {} connection", source.as_ref());
- let mcp_tools = McpToolSet::new(&records, self.mcp_store.clone()).await;
+ let mcp_tools = McpToolSet::new(&records, self.mcp_connection.clone()).await;
anyhow::ensure!(
!mcp_tools.is_empty(),
"could not load tools from {}",
@@ -990,11 +995,12 @@ where
}
}
-impl ImportService for ImportServiceImpl
+impl ImportService for ImportServiceImpl
where
R: ImportRepo + Clone,
S: McpServerStore,
C: EntityCreator,
+ P: McpConnection,
{
#[tracing::instrument(skip(self, user), err)]
async fn state(&self, user: MacroUserIdStr<'static>) -> Result {
@@ -1168,11 +1174,12 @@ where
}
}
-impl ImportStager for ImportServiceImpl
+impl ImportStager for ImportServiceImpl
where
R: ImportRepo + Clone,
S: McpServerStore,
C: EntityCreator,
+ P: McpConnection,
{
#[tracing::instrument(skip(self, user, metadata), err)]
async fn stage(
@@ -1311,11 +1318,12 @@ where
}
}
-impl NotionPageImporter for ImportServiceImpl
+impl NotionPageImporter for ImportServiceImpl
where
R: ImportRepo + Clone,
S: McpServerStore,
C: EntityCreator,
+ P: McpConnection,
{
#[tracing::instrument(skip(self, user), fields(page = page_url_or_id), err)]
async fn import_notion_page(
@@ -1441,11 +1449,12 @@ where
}
}
-impl ImportFinalizer for ImportServiceImpl
+impl ImportFinalizer for ImportServiceImpl
where
R: ImportRepo + Clone,
S: McpServerStore,
C: EntityCreator,
+ P: McpConnection,
{
#[tracing::instrument(skip(self, user, content_markdown), err)]
async fn finalize_document(
diff --git a/crates/macro_db_client/migrations/20260810143210_add_nango_connection_id_to_mcp_servers.sql b/crates/macro_db_client/migrations/20260810143210_add_nango_connection_id_to_mcp_servers.sql
new file mode 100644
index 00000000000..e4dbe026cea
--- /dev/null
+++ b/crates/macro_db_client/migrations/20260810143210_add_nango_connection_id_to_mcp_servers.sql
@@ -0,0 +1,6 @@
+-- MCP server authorization via Nango: servers connected through Nango store
+-- the Nango connection ID instead of encrypted OAuth credentials. Tokens are
+-- fetched fresh from Nango at connect time (Nango owns storage and refresh),
+-- so rows with a nango_connection_id keep `credentials` NULL.
+ALTER TABLE mcp_servers
+ ADD COLUMN nango_connection_id TEXT;
diff --git a/crates/macro_db_client/migrations/20260811160239_rebuild_mcp_servers_for_pipedream.sql b/crates/macro_db_client/migrations/20260811160239_rebuild_mcp_servers_for_pipedream.sql
new file mode 100644
index 00000000000..1fe99eb7f89
--- /dev/null
+++ b/crates/macro_db_client/migrations/20260811160239_rebuild_mcp_servers_for_pipedream.sql
@@ -0,0 +1,19 @@
+-- MCP connector auth moves wholesale to Pipedream Connect: Pipedream owns
+-- the OAuth grants and tokens, we store only which app a user connected and
+-- the Pipedream account ID the grant lives under. The previous shape (rows
+-- keyed by server URL carrying encrypted OAuth credentials, later a Nango
+-- connection ID) has no forward migration path — the in-house grants can't
+-- be transplanted into Pipedream — so the table is rebuilt and users
+-- reconnect through the new flow.
+DROP TABLE IF EXISTS mcp_servers;
+
+CREATE TABLE mcp_servers (
+ user_id TEXT NOT NULL,
+ app_slug TEXT NOT NULL,
+ server_name TEXT NOT NULL,
+ account_id TEXT NOT NULL,
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (user_id, app_slug)
+);
diff --git a/crates/mcp_client/Cargo.toml b/crates/mcp_client/Cargo.toml
index 37717ad6891..f9855667d2f 100644
--- a/crates/mcp_client/Cargo.toml
+++ b/crates/mcp_client/Cargo.toml
@@ -4,30 +4,18 @@ version = "0.1.0"
edition = "2024"
publish = false
-[features]
-default = ["providers"]
-providers = ["macro_env_var"]
-
[dependencies]
-aes-gcm = "0.10"
-agent = { path = "../agent" }
ai_toolset = { path = "../ai_toolset" }
anyhow.workspace = true
-async-trait.workspace = true
-oauth2 = { version = "5.0", default-features = false }
-base64.workspace = true
-async-openai.workspace = true
axum.workspace = true
futures.workspace = true
macro_authorization = { path = "../macro_authorization", default-features = false, features = [
"axum",
] }
-macro_env_var = { path = "../macro_env_var", optional = true }
macro_user_id = { path = "../macro_user_id" }
model-error-response = { path = "../model-error-response" }
-redis = { workspace = true, features = ["aio", "tokio-comp"] }
reqwest.workspace = true
-rmcp = { workspace = true, features = ["auth", "client", "transport-streamable-http-client-reqwest"] }
+rmcp = { workspace = true, features = ["client", "transport-streamable-http-client-reqwest"] }
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
diff --git a/crates/mcp_client/README.md b/crates/mcp_client/README.md
new file mode 100644
index 00000000000..00a8e0eae22
--- /dev/null
+++ b/crates/mcp_client/README.md
@@ -0,0 +1,66 @@
+# mcp_client
+
+MCP connector integration: users connect the apps their team uses (Linear,
+Notion, Slack, GitHub, …) and the AI agent calls those apps' tools. Hosted
+by `document_cognition_service`, which mounts the `/mcp/servers*` routes and
+builds per-request toolsets from the user's connected apps.
+
+## Pipedream: the single connect path
+
+[Pipedream Connect](https://pipedream.com/docs/connect) owns the entire
+account lifecycle: the consent flow (hosted Connect UI), OAuth apps for
+~2,500 providers (no per-provider OAuth app registration on our side),
+credential storage, and token refresh. Tool calls go through Pipedream's
+remote MCP server, which injects each account's credentials server-side —
+no tokens ever transit or persist in our systems. We store only the app
+slug and the Pipedream connected-account ID per user.
+
+There is deliberately **no fallback auth path**: no in-house OAuth, no
+per-provider client IDs. A deployment without Pipedream credentials answers
+501 on the connect/catalog endpoints and builds empty MCP toolsets.
+
+Flow:
+
+1. Frontend `POST /mcp/servers/pipedream/token` → short-lived Connect token
+ minted for the user.
+2. Frontend opens Pipedream's hosted Connect UI (iframe) for the chosen app;
+ the user authorizes; the UI reports a connected-account ID.
+3. Frontend `POST /mcp/servers/pipedream/complete` with the account ID. The
+ backend verifies with Pipedream that the account exists and was connected
+ for this user, then upserts the `mcp_servers` row and fires the
+ auth-completed hook (imports start immediately).
+
+Tool calls: `McpToolSet` connects to `remote.mcp.pipedream.net` per enabled
+app with the project's OAuth access token (client-credentials grant, cached)
+plus `x-pd-project-id` / `x-pd-environment` / `x-pd-external-user-id` /
+`x-pd-app-slug` headers and `x-pd-tool-mode: tools-only`.
+
+## Connector catalog
+
+`GET /mcp/servers/catalog` advertises what users can connect: a curated
+list of priority connectors (pinned first, flagged `priority` so clients
+can show them as a featured section) merged with search results from
+Pipedream's app directory. To promote a connector, add it to
+`PRIORITY_CONNECTORS` in `domain/service/catalog.rs`.
+
+## Setup (per environment)
+
+1. Create a Pipedream Connect project (pipedream.com), one per deploy
+ environment (its `development`/`production` split maps to ours).
+2. Add to doppler for `document_cognition_service`: `PIPEDREAM_CLIENT_ID`,
+ `PIPEDREAM_CLIENT_SECRET` (project OAuth client), and
+ `PIPEDREAM_PROJECT_ID` (`proj_...`).
+3. Optional: `PIPEDREAM_ENVIRONMENT` (defaults to `production` in prd,
+ `development` elsewhere), `PIPEDREAM_API_URL`, `PIPEDREAM_MCP_URL`.
+
+## Key pieces
+
+- `domain/service/toolset.rs` — `McpToolSet` / `CombinedToolSet`: connect
+ to the user's enabled apps, mangle tool names as `mcp____`,
+ dispatch calls.
+- `domain/service/pipedream_connect.rs` — connection-completion policy
+ (ownership verification) and disconnect.
+- `domain/service/catalog.rs` — the catalog merge and the curated priority
+ list.
+- `outbound/pipedream.rs` — the Pipedream REST + remote MCP adapter.
+- `inbound/axum_router.rs` — the `/mcp/servers*` HTTP surface.
diff --git a/crates/mcp_client/src/domain/mod.rs b/crates/mcp_client/src/domain/mod.rs
index f6993d267d1..96ce5f58c6d 100644
--- a/crates/mcp_client/src/domain/mod.rs
+++ b/crates/mcp_client/src/domain/mod.rs
@@ -2,8 +2,5 @@
pub mod models;
/// Port traits consumed by the domain service.
pub mod ports;
-/// Registry for MCP servers with pre-registered OAuth credentials.
-#[cfg(feature = "providers")]
-pub mod provider_registry;
/// Service orchestration for MCP connections and tool calls.
pub mod service;
diff --git a/crates/mcp_client/src/domain/models/aes_key.rs b/crates/mcp_client/src/domain/models/aes_key.rs
deleted file mode 100644
index ee9fa8de28e..00000000000
--- a/crates/mcp_client/src/domain/models/aes_key.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-/// A validated 32-byte AES-256 encryption key.
-#[derive(Clone)]
-pub struct AesKey([u8; 32]);
-
-impl AesKey {
- /// The raw key bytes.
- pub fn as_bytes(&self) -> &[u8; 32] {
- &self.0
- }
-}
-
-impl TryFrom> for AesKey {
- type Error = AesKeyError;
-
- #[tracing::instrument(skip_all, err)]
- fn try_from(bytes: Vec) -> Result {
- let bytes: [u8; 32] = bytes
- .try_into()
- .map_err(|v: Vec| AesKeyError::InvalidLength(v.len()))?;
- Ok(Self(bytes))
- }
-}
-
-impl TryFrom<&str> for AesKey {
- type Error = AesKeyError;
-
- /// Decode a base64-encoded key string into an [`AesKey`].
- #[tracing::instrument(skip_all, err)]
- fn try_from(b64: &str) -> Result {
- use base64::Engine;
- let bytes = base64::engine::general_purpose::STANDARD
- .decode(b64.trim())
- .map_err(AesKeyError::InvalidBase64)?;
- bytes.try_into()
- }
-}
-
-/// Errors when constructing an [`AesKey`].
-#[derive(Debug, thiserror::Error)]
-pub enum AesKeyError {
- /// Key must be exactly 32 bytes.
- #[error("AES-256 key must be exactly 32 bytes, got {0}")]
- InvalidLength(usize),
- /// Base64 decoding failed.
- #[error("invalid base64: {0}")]
- InvalidBase64(base64::DecodeError),
-}
diff --git a/crates/mcp_client/src/domain/models/catalog.rs b/crates/mcp_client/src/domain/models/catalog.rs
new file mode 100644
index 00000000000..683e4f1c722
--- /dev/null
+++ b/crates/mcp_client/src/domain/models/catalog.rs
@@ -0,0 +1,26 @@
+//! The public catalog of connectable MCP apps.
+
+/// One connectable app advertised in the catalog.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct CatalogEntry {
+ /// Pipedream app name slug, e.g. `linear` — what gets connected.
+ pub app_slug: String,
+ /// Human-readable name to display, e.g. `Linear`.
+ pub display_name: String,
+ /// One-line description of what connecting the app enables.
+ pub description: Option,
+ /// URL of the app's icon, when the directory provides one.
+ pub icon_url: Option,
+ /// Whether this is a curated priority connector, ranked above organic
+ /// directory results (and renderable as its own section).
+ pub priority: bool,
+}
+
+/// One page of catalog results.
+#[derive(Clone, Debug, Default)]
+pub struct CatalogPage {
+ /// The entries on this page, in display order.
+ pub entries: Vec,
+ /// Opaque cursor for fetching the next page. `None` on the last page.
+ pub next_cursor: Option,
+}
diff --git a/crates/mcp_client/src/domain/models/mod.rs b/crates/mcp_client/src/domain/models/mod.rs
index fac1157fe84..a1eb617d05c 100644
--- a/crates/mcp_client/src/domain/models/mod.rs
+++ b/crates/mcp_client/src/domain/models/mod.rs
@@ -1,15 +1,14 @@
-mod aes_key;
mod call_tool_result;
+mod catalog;
mod consts;
-mod oauth_client_metadata;
+mod pipedream;
mod result;
mod server;
-pub use aes_key::{AesKey, AesKeyError};
pub use call_tool_result::CallToolResultExt;
+pub use catalog::{CatalogEntry, CatalogPage};
pub use consts::*;
pub use macro_user_id::user_id::MacroUserIdStr;
-pub use oauth_client_metadata::OAuthClientMetadata;
+pub use pipedream::{ConnectToken, PipedreamAccount};
pub use result::{Error, Result};
-pub use rmcp::transport::auth::StoredCredentials;
-pub use server::{McpServer, McpServerConnectionInfo, McpServerRecord, client_info};
+pub use server::{McpServer, McpServerRecord, client_info};
diff --git a/crates/mcp_client/src/domain/models/oauth_client_metadata.rs b/crates/mcp_client/src/domain/models/oauth_client_metadata.rs
deleted file mode 100644
index 62643908c24..00000000000
--- a/crates/mcp_client/src/domain/models/oauth_client_metadata.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use super::MCP_CLIENT_NAME;
-
-/// Public OAuth client metadata used when an authorization server supports
-/// Client ID Metadata Documents (CIMD).
-#[derive(Clone, Debug, serde::Serialize)]
-pub struct OAuthClientMetadata {
- client_id: String,
- client_name: String,
- redirect_uris: Vec,
- grant_types: Vec,
- response_types: Vec,
- token_endpoint_auth_method: String,
-}
-
-impl OAuthClientMetadata {
- /// Build Macro's public OAuth client metadata document.
- pub fn new(client_id: String, redirect_uri: String) -> Self {
- Self {
- client_id,
- client_name: MCP_CLIENT_NAME.to_string(),
- redirect_uris: vec![redirect_uri],
- grant_types: vec![
- "authorization_code".to_string(),
- "refresh_token".to_string(),
- ],
- response_types: vec!["code".to_string()],
- token_endpoint_auth_method: "none".to_string(),
- }
- }
-
- /// Return the HTTPS URL that identifies Macro to CIMD-capable servers.
- pub fn client_id(&self) -> &str {
- &self.client_id
- }
-
- /// Return the callback URI used for authorization-code redirects.
- pub fn redirect_uri(&self) -> &str {
- self.redirect_uris
- .first()
- .expect("OAuthClientMetadata always has one redirect URI")
- }
-}
-
-#[cfg(test)]
-mod test;
diff --git a/crates/mcp_client/src/domain/models/oauth_client_metadata/test.rs b/crates/mcp_client/src/domain/models/oauth_client_metadata/test.rs
deleted file mode 100644
index 9813ed91929..00000000000
--- a/crates/mcp_client/src/domain/models/oauth_client_metadata/test.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use super::*;
-
-#[test]
-fn serializes_required_client_id_metadata_fields() {
- let metadata = OAuthClientMetadata::new(
- "https://document-cognition.macro.com/mcp/servers/auth/client-metadata".to_string(),
- "https://document-cognition.macro.com/mcp/servers/auth/callback".to_string(),
- );
-
- let value = serde_json::to_value(metadata).expect("metadata serializes");
-
- assert_eq!(
- value,
- serde_json::json!({
- "client_id": "https://document-cognition.macro.com/mcp/servers/auth/client-metadata",
- "client_name": "Macro",
- "redirect_uris": ["https://document-cognition.macro.com/mcp/servers/auth/callback"],
- "grant_types": ["authorization_code", "refresh_token"],
- "response_types": ["code"],
- "token_endpoint_auth_method": "none",
- })
- );
-}
diff --git a/crates/mcp_client/src/domain/models/pipedream.rs b/crates/mcp_client/src/domain/models/pipedream.rs
new file mode 100644
index 00000000000..b445e4b1486
--- /dev/null
+++ b/crates/mcp_client/src/domain/models/pipedream.rs
@@ -0,0 +1,27 @@
+//! Domain models for the Pipedream Connect integration.
+
+/// A short-lived token for opening Pipedream's hosted Connect UI.
+#[derive(Clone, Debug)]
+pub struct ConnectToken {
+ /// The Connect token itself.
+ pub token: String,
+ /// RFC 3339 expiry of the token.
+ pub expires_at: String,
+ /// Shareable link that opens the same connect flow in a browser tab.
+ pub connect_link_url: String,
+}
+
+/// A connected account as reported by Pipedream.
+#[derive(Clone, Debug)]
+pub struct PipedreamAccount {
+ /// Pipedream's connected-account ID (`apn_...`).
+ pub id: String,
+ /// The external user ID the account was connected for (our user ID).
+ pub external_user_id: Option,
+ /// The app the account belongs to (name slug, e.g. `linear`).
+ pub app_slug: String,
+ /// Human-readable app name, e.g. `Linear`.
+ pub app_name: String,
+ /// Whether Pipedream considers the account's credentials healthy.
+ pub healthy: bool,
+}
diff --git a/crates/mcp_client/src/domain/models/result.rs b/crates/mcp_client/src/domain/models/result.rs
index 7cf068e33b9..8ba13401472 100644
--- a/crates/mcp_client/src/domain/models/result.rs
+++ b/crates/mcp_client/src/domain/models/result.rs
@@ -1,33 +1,17 @@
-use macro_env_var::VarNameErr;
use thiserror::Error;
/// Domain errors for the MCP client.
#[derive(Debug, Error)]
pub enum Error {
- /// The server requires authentication but no credentials are stored.
- #[error("no credentials stored for server: {0}")]
- NoCredentials(String),
- /// Stored credentials are invalid or expired.
- #[error("invalid credentials for server: {0}")]
- InvalidCredentials(String),
- /// Failed to connect to the MCP server.
- #[error("connection failed: {0}")]
- Connection(String),
/// The requested tool was not found on any connected server.
#[error("unknown tool: {0}")]
UnknownTool(String),
/// A tool invocation failed.
#[error("tool call failed: {0}")]
ToolCall(String),
- /// A mangled tool name already exists in the tool set.
- #[error("tool name conflict: {0}")]
- ToolConflict(String),
/// An internal or infrastructure error.
#[error(transparent)]
Internal(#[from] anyhow::Error),
- /// A required env var is missing
- #[error("required environment variable not provided: {0}")]
- RequiredEnvironmentVariable(VarNameErr),
}
/// Domain result type.
diff --git a/crates/mcp_client/src/domain/models/server.rs b/crates/mcp_client/src/domain/models/server.rs
index 511998c7baa..c750876f5f5 100644
--- a/crates/mcp_client/src/domain/models/server.rs
+++ b/crates/mcp_client/src/domain/models/server.rs
@@ -1,15 +1,8 @@
use super::consts::MCP_CLIENT_NAME;
-use crate::domain::ports::{McpConnector, McpServerStore};
-use crate::domain::service::PersistingCredentialStore;
use macro_user_id::user_id::MacroUserIdStr;
use rmcp::RoleClient;
use rmcp::model::{ClientInfo, Implementation};
-use rmcp::service::{RunningService, ServiceExt};
-use rmcp::transport::StreamableHttpClientTransport;
-use rmcp::transport::auth::{AuthClient, AuthorizationManager, StoredCredentials};
-use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
+use rmcp::service::RunningService;
/// A connected MCP server session.
pub type McpServer = RunningService;
@@ -22,54 +15,21 @@ pub fn client_info() -> ClientInfo {
)
}
-/// Connection details for an MCP server.
-#[derive(Clone, Debug, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub struct McpServerConnectionInfo {
- /// Human-readable server name.
- pub name: String,
- /// The server's streamable HTTP URL.
- pub url: String,
-}
-
-/// A persisted MCP server entry with connection info and credentials.
-#[derive(Clone, Debug, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
+/// An MCP connector a user has connected through Pipedream.
+///
+/// Pipedream owns the OAuth grant and tokens for the connected account; we
+/// store only which app the user connected and the Pipedream account ID the
+/// grant lives under.
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct McpServerRecord {
- /// The user who owns these credentials.
+ /// The user who connected the app.
pub user_id: MacroUserIdStr<'static>,
- /// The server URL these credentials authenticate against.
- pub url: String,
- /// Name of the MCP server.
+ /// Pipedream app name slug, e.g. `linear` or `notion`.
+ pub app_slug: String,
+ /// Human-readable display name, e.g. `Linear`.
pub server_name: String,
- /// The OAuth credentials.
- #[serde(skip)]
- pub credentials: Option,
- /// Whether the user has this toolset enabled.
+ /// The Pipedream connected-account ID holding the grant.
+ pub account_id: String,
+ /// Whether the connector is enabled for tool use.
pub enabled: bool,
}
-
-impl McpConnector for McpServerRecord {
- #[tracing::instrument(skip_all, err)]
- async fn connect(&self, server_store: Arc) -> anyhow::Result {
- match &self.credentials {
- Some(credentials) => {
- let mut auth_manager = AuthorizationManager::new(&self.url).await?;
- let store = PersistingCredentialStore::new(self.clone(), server_store);
- store.seed(credentials.clone()).await?;
- auth_manager.set_credential_store(store);
- auth_manager.initialize_from_store().await?;
-
- let auth_client = AuthClient::new(reqwest::Client::new(), auth_manager);
- let config = StreamableHttpClientTransportConfig::with_uri(&*self.url);
- let transport = StreamableHttpClientTransport::with_client(auth_client, config);
-
- Ok(client_info().serve(transport).await?)
- }
- None => {
- let transport = StreamableHttpClientTransport::from_uri(&*self.url);
- Ok(client_info().serve(transport).await?)
- }
- }
- }
-}
diff --git a/crates/mcp_client/src/domain/ports.rs b/crates/mcp_client/src/domain/ports.rs
index ad4b83a426c..ee2fd253a5b 100644
--- a/crates/mcp_client/src/domain/ports.rs
+++ b/crates/mcp_client/src/domain/ports.rs
@@ -1,109 +1,98 @@
-use super::models::{MacroUserIdStr, McpServer, McpServerRecord};
-use std::sync::Arc;
+use super::models::{
+ CatalogPage, ConnectToken, MacroUserIdStr, McpServer, McpServerRecord, PipedreamAccount,
+};
-/// Port for persisting MCP server records, keyed by user.
+/// Port for persisting connected MCP apps, keyed by user and app slug.
pub trait McpServerStore: Send + Sync + 'static {
/// Error type for store operations.
type Err: Send + std::fmt::Debug;
- /// Persist a server record, overwriting any existing entry for the same user and URL.
+ /// Persist a record, overwriting any existing entry for the same user and app.
fn save(&self, record: &McpServerRecord) -> impl Future