Connect end-user accounts with ProjectConnector
Use ProjectConnector when every user of your product needs to connect their own provider account. Your backend identifies each user with externalUserId, creates authorization links, stores connected account IDs, and executes actions for that user.
This path uses a project API key shaped like oo_proj_…. It is separate from the personal api_… key used by Connector.
Connect your Agent
ProjectConnector already handles calls to the connector service. OOMOL handles provider authorization, credential storage, token refresh, and action execution. Your product supplies user authentication, account bindings, and Agent tool handlers.
If your Agent runs on your backend, its tool handlers can call ProjectConnector directly. If it runs on a user device, send tool requests to your authenticated product backend. Keep the project API key on that backend.
The hosted personal/team MCP configuration uses OOMOL user and team connections. For SaaS end-user accounts, use ProjectConnector or the SaaS HTTP API. A client that requires MCP needs a product-specific, authenticated MCP adapter on your backend.
Prepare the project
Before writing the runtime flow, create these resources in OOMOL Console:
- A Connector project.
- A provider config for each service users can connect.
- A project API key stored in your backend secret manager.
The Connector for SaaS guide covers Console setup and the matching REST requests.
Install and initialize
npm install @oomol-lab/connector
import { ProjectConnector } from "@oomol-lab/connector";
const project = new ProjectConnector({
apiKey: process.env.OOMOL_PROJECT_API_KEY!,
});
Create an OAuth authorization request
Use a stable ID from your own user database as externalUserId:
const request = await project.connect.oauth("user_42", {
service: "gmail",
connectionName: "work",
returnUri: "https://app.example.com/connected",
});
redirectUserTo(request.authorizationUrl);
After the user authorizes, wait for the request to reach a final state:
const connected = await project.waitForConnection(request);
if (connected.status === "connected") {
saveConnectedAccountId(connected.connectedAccountId);
}
project.connect.oauth returns an authorization request that waits for the user. Only after authorization succeeds does waitForConnection return a connectedAccountId in the request result.
For API-key and custom-credential providers, use connect.apiKey or connect.customCredential. These methods validate the credential and return a connected account synchronously.
Execute for one user
const result = await project.execute(
"user_42",
"gmail.search_threads",
{ query: "is:unread" },
{ connectedAccountId: "ca-1" },
);
Pass connectedAccountId when you have it. It selects one specific account and avoids relying on the latest active account. connectionName is available when your product uses stable aliases instead.
Bind the user once with forUser when several operations belong to the same request or job:
The Slack example requires an existing connection named “work” for this user. Use the connection name stored by your backend for that Slack account. forUser binds only the user; account selection belongs in execute options.
const user = project.forUser("user_42");
await user.execute(
"slack.post_message",
{ channel: "#general", text: "shipped" },
{ connectionName: "work" },
);
Expose an action as an Agent tool
This framework-independent descriptor shows the schema and handler to register with your Agent framework. Before creating it, authenticate the request, check that the user may use this action, and load their Gmail account binding from your database. currentUser.id is your trusted external user ID; storedAccount.connectedAccountId belongs to that user. Neither value comes from model-generated arguments.
const searchMailTool = {
name: "search_mail",
description: "Search the current user's Gmail threads.",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
additionalProperties: false,
},
execute: async (input: { query: string }) =>
project.forUser(currentUser.id).execute(
"gmail.search_threads",
input,
{ connectedAccountId: storedAccount.connectedAccountId },
),
};
Validate tool arguments against the schema before calling the handler. The model supplies only the search query. Start with the actions your product needs and filter by the user’s connected services; the SDK does not automatically register all provider actions as Agent tools.
Keep product boundaries explicit
Your product authenticates its own users and controls which providers and actions they can use. Keep the project API key on the backend, pass a consistent externalUserId, and store the returned account selector beside the corresponding product user.
See the TypeScript SDK reference for authorization-request and connected-account lifecycle fields, precise action types, errors, retries, waiting options, and the complete ProjectConnector API.