Connectors for Seed agents
How agents connect to Salesforce, a SQL database, GitHub, whatever, without us or the model ever seeing the user's key. Who runs what, what we have, what we build.

Problem

The PDF importer works because we hold one Datalab key for everybody (PDF Importer). Salesforce is the opposite. Every company has its own org, every user its own login, nobody pastes a token into an agent. Same with a company database. And we can't ship one builtin per service. Horacio wants the same for GitHub (Github Connector proposal), so this is a connector thing, not a Salesforce thing.

What a connector is

"An API key is not a tool" (meeting notes). So three objects, three owners:

    The template. A public Seed document. Says which executor (mcp, sql, later code), where the service is, how to log in (oauth or secret), what the user has to fill at install, and a guide or an ontology for the agent. No code, except code connectors, there the TS goes in the body. Here is one, the Sales database template.

    The credential. Private, belongs to one space, encrypted in the agents server. Never in the template, never in the model.

    The executor. Generic code in the agents server binary. mcp talks to any MCP server, sql talks to any MySQL, code runs the template's TS in the sandbox. Same code hosted, on the laptop, self hosted.

Anyone with the template and their own credential gets the functionality.

Who runs what

We run one thing: the agents server. It is already an MCP client (MCP servers). There is no "local client plus a connector process". Salesforce runs the Salesforce MCP server, GitHub runs theirs, the database is just the database. The app is UI: Install, Sign in, chat. On the desktop it also catches the OAuth redirect on localhost and hands the code to the agents server, exactly how "Sign in with ChatGPT" works today. The model provider gets text, tool definitions and results. Never a token, never a DSN.

Green is what we have, orange is what we build, grey is not ours.

%%{init: {"theme":"base","themeVariables":{"fontSize":"15px","primaryColor":"#e8e8e8","primaryTextColor":"#111111","primaryBorderColor":"#777777","lineColor":"#8a8a8a","textColor":"#111111","nodeTextColor":"#111111","edgeLabelBackground":"#f0f0f0","clusterBkg":"#f4f4f4","clusterBorder":"#aaaaaa","titleColor":"#111111"}}}%% flowchart LR classDef have fill:#8fe08f,stroke:#1b7a3a,color:#111 classDef todo fill:#ffb866,stroke:#c45c00,color:#111 classDef ext fill:#f2f2f2,stroke:#999,stroke-dasharray:5 5,color:#333 subgraph app["Seed app (desktop / web)"] chat["Chat, signed actions"]:::have catch["Catch OAuth redirect on localhost<br/>(ChatGPT login today)"]:::have tabs["Tools tab: Ready / Needs sign in /<br/>Needs secret / Update available"]:::todo install["Install button on a template page<br/>Publish as template"]:::todo end subgraph srv["Agents server, one binary: hosted / laptop / self hosted"] rt["Runtime: prompt, tool index, call"]:::have disc["MCP client + tool projection<br/>server__tool documents"]:::have sec[("Encrypted secrets by reference")]:::have pkce["OAuth PKCE for ChatGPT<br/>provider-oauth.ts"]:::have oauth["OAuth client for MCP servers<br/>(generalize pkce, persist logins,<br/>https callback on hosted)"]:::todo inst["InstallConnector from hm://<br/>pin CID, records, grant,<br/>per install secret namespace"]:::todo sql["sql executor<br/>query + schema, read only tx, caps"]:::todo code["code executor (later)<br/>TS in the sandbox, proxy adds the token"]:::todo mode["Grant mode read / all<br/>destructive asks"]:::todo broker["Broker for client secret exchange<br/>(GitHub), hosted only"]:::todo cimd["One JSON at hyper.media<br/>(CIMD client id)"]:::todo end llm["Model provider"]:::ext sf["Salesforce hosted MCP"]:::ext gh["GitHub remote MCP"]:::ext db[("MySQL")]:::ext api["Any HTTP API"]:::ext net["Seed network: templates"]:::ext chat --> rt install --> inst catch --> oauth rt -- "text, tool defs, results. never secrets" --> llm inst -.-> net inst --> sec oauth --> sec pkce -. same code .-> oauth disc -- "tools/list, tools/call + token" --> sf disc --> gh disc --> sec rt --> disc rt --> sql sql --> sec sql --> db rt --> code code --> sec code --> api rt --> mode oauth -.-> broker oauth -.-> cimd

Where the functions come from

Not from the template, except for code.

For MCP connectors the MCP server defines them. On connect the agents server asks tools/list and gets names, descriptions and input schemas. Each one becomes a tool document in the agent's tools folder, named server__tool, so Salesforce gives you salesforce__run_soql, salesforce__get_record (Tool document). This exists today. If the template listed them it would be stale the day Salesforce adds one.

For SQL there is no server to ask, so our executor has two fixed functions, query and schema. The template has the ontology in plain words, "Customer: name, country, segment". The agent reads the ontology, reads the real tables with schema, and writes the SELECT. The ontology says what the fields mean, the database says how they are called.

For a service without MCP the connector is code. TS, same sandbox as the lambdas today, no mini language. The template carries the code in the body and install drops it in the tools folder as lambdas. The code never holds the token, it calls the service through a proxy in the agents server that adds the auth header for the host of that credential. After MCP.

What a template looks like for Salesforce:

name: Salesforce executor: mcp service: url: "{mcpUrl}" auth: oauth params: mcpUrl: The MCP url of your org, from Setup > Hosted MCP Servers clientId: Consumer key of the External Client App your admin created mode: read

plus a guide in the body. Same keys for GitHub with url: https://api.githubcopilot.com/mcp/. Same keys for SQL with executor: sql, auth: secret and the ontology in the body. Same keys for code with executor: code and the TS in a code block of the body.

Can we do SSO?

This is a feature most integrators request. And yes, we could do it. What they call SSO is OAuth authorization code with PKCE: the user logs in at Salesforce in the browser, Salesforce sends us a code, we exchange it for a token. We never see the password. We hold a refresh token with a small scope that the user can revoke from Salesforce any day. We already run exactly this flow for "Sign in with ChatGPT" (Model providers), it just needs to be generalized to MCP servers.

Generalized means one thing: we follow the MCP authorization spec, which is this same OAuth 2.1 code plus PKCE flow. One code path for Salesforce, GitHub, Linear, Notion.

The only branch is where the client_id comes from, and the service decides it, not us, not the template. Three answers: a pre registered app (Salesforce only accepts this, its External Client App with mcp_api and refresh_token, PKCE, no secret; GitHub too), CIMD (the client id is a URL to a JSON we host once at hyper.media, the new spec default), or dynamic registration (we POST once). The SDK we ship reads the service metadata and picks. The template author writes auth: oauth and nothing more. The user sees a Sign in button, and a client id field only for Salesforce style services, where the admin creates the app anyway.

One exception. GitHub wants a client secret when exchanging the code. A laptop can't hold that, so that one step goes through a broker in our hosted server, same relay as convert.

%%{init: {"theme":"base","themeVariables":{"fontSize":"15px","primaryColor":"#e8e8e8","primaryTextColor":"#111111","primaryBorderColor":"#777777","lineColor":"#8a8a8a","textColor":"#111111","nodeTextColor":"#111111","edgeLabelBackground":"#f0f0f0","clusterBkg":"#f4f4f4","clusterBorder":"#aaaaaa","titleColor":"#111111"}}}%% flowchart TD classDef have fill:#8fe08f,stroke:#1b7a3a,color:#111 classDef todo fill:#ffb866,stroke:#c45c00,color:#111 classDef ext fill:#f2f2f2,stroke:#999,stroke-dasharray:5 5,color:#333 a["Template says only: the MCP url<br/>and auth: oauth"]:::todo --> b["Agents server GETs the url"]:::have b --> c{"401?"}:::have c -- no --> d["No auth. Ready."]:::have c -- yes --> e["Standard MCP discovery:<br/>resource metadata, then<br/>authorization server metadata<br/>(in the SDK we ship)"]:::have e --> f{"Where does the client_id come from?<br/>The SERVICE decides, the SDK picks"}:::have f -- "install parameter" --> g["Pre registered app<br/>Salesforce External Client App<br/>GitHub OAuth app"]:::ext f -- "service supports CIMD" --> h["CIMD: client_id is an<br/>https url we host once"]:::todo f -- "service has registration endpoint" --> i["DCR: POST once,<br/>get a client_id"]:::have f -- "none" --> j["Tools tab: Needs client id"]:::todo g --> k h --> k i --> k k["ONE flow for everybody<br/>OAuth 2.1 code + PKCE<br/>user logs in in the browser"]:::todo --> l["Tokens encrypted in the install's<br/>namespace, bound to the host.<br/>Ready."]:::todo

Install and sign in

Once per space. Install is a signed action from a writer of the space: fetch the template, pin its version, create the MCP server record, the grant and an empty secret namespace. Sign in is the OAuth flow above. After that, discovery runs as today and the connector is Ready.

%%{init: {"theme":"base","themeVariables":{"primaryColor":"#e8e8e8","primaryTextColor":"#111111","primaryBorderColor":"#777777","lineColor":"#8a8a8a","textColor":"#111111","actorBkg":"#f0f0f0","actorBorder":"#777777","actorTextColor":"#111111","actorLineColor":"#9a9a9a","signalColor":"#333333","signalTextColor":"#111111","labelBoxBkgColor":"#ffffff","labelBoxBorderColor":"#555555","labelTextColor":"#111111","loopTextColor":"#111111","noteBkgColor":"#ffffff","noteBorderColor":"#555555","noteTextColor":"#111111","sequenceNumberColor":"#ffffff"},"sequence":{"messageFontSize":17,"actorFontSize":17,"noteFontSize":16,"actorMargin":36,"width":150,"messageMargin":28,"mirrorActors":false,"boxMargin":8}}}%% sequenceDiagram autonumber participant U as User participant App as Seed app participant S as Agents server participant SF as Salesforce rect rgb(255, 184, 102) Note over U,SF: TO BUILD: install U->>App: Install template App->>S: InstallConnector (signed) S->>S: fetch template, pin CID, record + grant S-->>App: Needs sign in end rect rgb(255, 184, 102) Note over U,SF: TO BUILD: OAuth on MCP servers U->>App: Sign in App->>S: StartConnectorOAuth (signed) S->>SF: GET MCP url, no token SF-->>S: 401 + auth metadata S-->>App: authorize url (PKCE) App->>SF: browser login SF-->>App: code (localhost or https callback) App->>S: SubmitConnectorOAuthCode (signed) S->>SF: code + verifier SF-->>S: access + refresh token S->>S: store encrypted, bound to host end rect rgb(143, 224, 143) Note over S,SF: HAVE: discovery and projection S->>SF: tools/list (bearer) SF-->>S: tools + input schemas S->>S: tool documents salesforce__tool S-->>App: Ready end

A call

Every time the agent uses it. The token exists in one place, inside the executor, between reading it and sending the request. The model gets the data back, never the token. The data does go to the model provider, that is the point of the connector, and people should know it.

%%{init: {"theme":"base","themeVariables":{"primaryColor":"#e8e8e8","primaryTextColor":"#111111","primaryBorderColor":"#777777","lineColor":"#8a8a8a","textColor":"#111111","actorBkg":"#f0f0f0","actorBorder":"#777777","actorTextColor":"#111111","actorLineColor":"#9a9a9a","signalColor":"#333333","signalTextColor":"#111111","labelBoxBkgColor":"#ffffff","labelBoxBorderColor":"#555555","labelTextColor":"#111111","loopTextColor":"#111111","noteBkgColor":"#ffffff","noteBorderColor":"#555555","noteTextColor":"#111111","sequenceNumberColor":"#ffffff"},"sequence":{"messageFontSize":17,"actorFontSize":17,"noteFontSize":16,"actorMargin":36,"width":150,"messageMargin":28,"mirrorActors":false,"boxMargin":8}}}%% sequenceDiagram autonumber participant U as User participant S as Agents server participant M as Model provider participant SF as Salesforce MCP rect rgb(143, 224, 143) Note over U,M: HAVE: runtime U->>S: which accounts closed this month? S->>M: prompt + tool index M-->>S: call salesforce__run_soql end rect rgb(255, 184, 102) Note over S,M: TO BUILD: mode read / all S->>S: grant ok? mode read? end rect rgb(143, 224, 143) Note over S,SF: HAVE: secrets by reference, MCP call S->>S: token of this install, executor only S->>SF: tools/call + bearer SF-->>S: rows S->>M: result, never the token M-->>S: answer S-->>U: answer end Note over S,SF: SQL: same flow, the sql executor is TO BUILD

Two rules on secrets. The template declares roles (oauth, dsn), never secret names, and install creates the secrets in a namespace of that install. Otherwise a bad template says "my secret is called openai-subscription-oauth" and gets your ChatGPT token. And the credential remembers the host it was made for, a template update that changes the host drops it.

Where it lives

In the gateway a custom domain (example.com) and a subdomain (example.hyper.media) resolve to one space (Sites). Same unit here: a connector is installed in a space. The agents server never sees a hostname, only the signed envelope with the account and the capability, and that is enough. Builtins are visible everywhere. A space's connectors are visible only to agents attached to that space (Agent Permissions) and enabled by a writer. Nothing from another space on the same server, not even for the same person.

Does the agents server even understand this? It is not a node. No libp2p, no sync, it reads documents over HTTP from one Seed server, anonymous. But it is a real Seed client, same library as seed-cli. Every request is a signed envelope and the server checks the ed25519 signature itself. If the signer is not the account, the envelope names a Capability blob, and the server decodes it, checks the CID, checks its signature, and only accepts WRITER or AGENT (agents/src/auth.ts). Every agent has its own key, the server publishes with it after checking the capability on the space, and every table is keyed by that account. So install in a space is one more signed action plus a record. Zero protocol work.

Two things it can't do today, and the first version doesn't need. Read private documents: reads are anonymous, so a private template is invisible to it, public templates work now, private ones wait for signed reads. And verify who wrote a template: it trusts what the gateway returns, the code to check the change blobs is there, same as for capabilities, nobody calls it on reads. Small, worth doing for the marketplace.

Visibility of a template is the visibility of the document. Private document, private connector, only who can read it can install it. Total control, self host: a site already says which agents server it uses (agentServerUrl).

Grants and injection

Enabling a connector is as strong as execute (MCP servers). First version: connectors get a mode, read or all, from the annotations MCP tools already carry, destructive tools ask before running, and OAuth connectors run only in sessions of the space owner and writers, because a Salesforce record can carry instructions for the agent (Security).

The marketplace

A template is a typed document, "Connector template", in the author's space. Any site with a query over that type is a marketplace. The signature is the trust, comments are the reviews, the version is the release.

The template page has an Install button: pick the space and the agents, fill the params, confirm. The Tools tab shows every connector of the space with a state: Ready, Needs sign in, Needs secret, Unreachable, Update available. Only Ready tools go to the agent's index. To make your own: point an agent at your MCP server or your database, try it, then "Publish as template" into your space, the server writes the document from the tool bytes minus the credential.

Have and build

Have: the runtime and call, the MCP client and the server__tool projection, encrypted secrets by reference, the PKCE flow for ChatGPT, signed actions, the localhost catch in the desktop, the Unreachable and Auth chips in the Tools tab.

Build:

    OAuth client for MCP servers: generalize the ChatGPT one, persist pending logins, an https callback on the hosted server.

    Install from hm://: fetch, pin, records, grant, per install secret namespace.

    The sql executor: query and schema, read only transaction, timeout, row and byte caps.

    Tools tab states, Install button, Publish as template, grant mode read / all.

    The broker for client secret exchange, hosted only.

    One JSON at hyper.media for CIMD.

    The code executor: TS from the template run as lambdas, plus the proxy that adds the auth header so the code never sees the token. Last.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime