ChloeGet Started

Docs

Connections

Google and Resend ship with chloe, a service's MCP server is one line in an agent, and an account chloe does not ship is a file of your own.


A connection is an outside account a tool works through, like Google for mail or Resend for sending. Google and Resend ship with chloe. A tool says which one it needs, and the rest follows from that: the setup page and the lines printed at startup say what each connection is still missing, and when a tool finds nobody signed in, chloe runs the sign-in itself.

So an agent names gmailReadEmail and is done. It cannot have mail without the means to sign in to mail.

Google

One sign-in covers Gmail, Calendar and Drive. chloe talks to Google with plain web requests, so there is nothing to install beside it.

Every copy of chloe signs in with a Google app of its own, made once:

  1. At console.cloud.google.com, make a project, and switch on the Gmail API, the Google Calendar API and the Google Drive API.
  2. In the Google Auth Platform section, give the app a name, choose External, and under Audience press Publish app so it is In production. Left in Testing, Google ends the sign-in every 7 days.
  3. Make an OAuth client of the Web application type, and add the redirect addresses chloe lists for it, exactly as written.
  4. Download the client file and hand it over as connections.google.client, a key like any other: its path or its contents in .env as CHLOE_CONNECTIONS_GOOGLE_CLIENT, and the config naming it, with the account beside it.
settings: {
  connections: {
    google: { account: "you@example.com", client: process.env.CHLOE_CONNECTIONS_GOOGLE_CLIENT },
  },
}

Then ask the agent for your mail. chloe sees that nobody has signed in and sends a link, exactly as Google needs it; you approve, and send back the short code the page you land on shows. chloe finishes the sign-in, checks that Google allowed everything it asked for, and answers what you asked. The model never sees the link or the code. The sign-in is one file in the state folder, mode 600.

The Google tools

Each one is bound in the agent's own agent.ts. The binding says what the tool may see, and the model chooses only how far back, how many, or what to look for. A binding a model cannot write is a boundary; a query it can write is only a filter.

import * as calendar from "@chloejs/core/tools/calendar";
import * as drive from "@chloejs/core/tools/drive";
import * as gmail from "@chloejs/core/tools/gmail";

const PLANS = "'1AbC...' in parents"; // one Drive folder, by its id

tools: {
  gmailReadEmail: gmail.readEmail({ search: "in:inbox label:orders", what: "the order mail" }),
  gmailReplyEmail: gmail.replyEmail({ search: "in:inbox label:orders", what: "the order mail" }),
  calendarListEvents: calendar.listEvents({ calendars: ["primary"] }),
  calendarAddEvent: calendar.addEvent({ calendar: "primary" }),
  driveSearchFiles: drive.searchFiles({ search: PLANS, what: "the plans folder" }),
  driveReadFile: drive.readFile({ search: PLANS, what: "the plans folder" }),
}
Tool Bound to What the model may do
gmailReadEmail search, a Gmail query. Unsaid it is the whole inbox. List that mail, and read one message it listed.
gmailReplyEmail the same search Answer a message it already listed, at that message's own address.
gmailSendEmail from, to and when Send to the addresses it was given, as the signed-in account.
calendarListEvents calendars, by id. Unsaid it is primary. List the events from a day it picks, up to 90 days ahead.
calendarAddEvent calendar Add one event. It invites nobody, so nothing reaches anybody else.
driveSearchFiles search, a Drive query. Unsaid it is every file the account can open. Find files in it, by words or newest first.
driveReadFile the same search Read a file the search listed, as text: a Google Doc, Sheet or Slides, or a text file.

A job reaches the same work without a model, from @chloejs/core/services: readEmailMessages, sendGmail, listCalendarEvents, addCalendarEvent, searchDriveFiles and readDriveFile.

Resend

Resend sends mail on a key, from an address on a domain you own: connections: { resend: { api_key: process.env.CHLOE_CONNECTIONS_RESEND_API_KEY } } in the config, the key in .env, and resend.sendEmail() from @chloejs/core/tools/resend in the agent's tools. Without a key nothing is sent and nothing fails.

Connections: a service's MCP server

Many services publish their tools for models as an MCP server: a list of tools reached over HTTP. An agent lists the ones it may use in connections, and only that agent gets their tools.

import { mcpConnection } from "@chloejs/core/connections";

connections: [
  mcpConnection({
    name: "github",
    url: "https://api.githubcopilot.com/mcp/",
    token: process.env.GITHUB_TOKEN,
    tools: ["list_issues", "get_issue"],
  }),
],

The tools are asked for as the agent loads, and each is named like chloe's own: github and list_issues make githubListIssues. tools keeps only the ones named; without it the agent gets every tool the server has. A server that does not answer leaves the agent loading without its tools, and the setup page says why. token is sent as a bearer key, and may be a function that fetches one when it is needed; headers is for a service that wants something else.

A connection reaches whatever its key reaches. Use one for a service where that is what the agent should have. Mail, notes and files stay chloe's own tools, bound to what one agent may see.

A connection of your own

An account chloe does not ship is a folder in the agent's own folder: its services, its tools, and a Connection saying what it needs. Nothing in the runtime has to change.

/**
 * An outside account a tool works through. A tool names it as its `needs`, and
 * the runtime asks it what is missing and runs its sign-in.
 */
export interface Connection {
  /** What the setup page calls it, and what `NeedsSignIn` names. */
  name: string;
  /** What it is for, in one line. */
  does: string;
  /** The settings it reads, as paths. Never their values. */
  settings: string[];
  /** How somebody signs in from a chat, when it can be done from one. */
  signIn?: SignIn;
  /** What is missing before it works, one line each, in words. Empty when it is ready. */
  missing(): Promise<string[]>;
}

missing() is what the setup page and the startup lines show, one line each, and an empty list means ready. signIn is optional: a connection on a plain key has none. One that has it gives chloe three functions, start() for the words and the link, answers() to know the reply when it comes, and finish(), and its service throws NeedsSignIn when signing in would fix what failed. chloe does the rest, on every channel. A tool that works through it says so with needs:

export function crmFindCustomer() {
  return Object.assign(tool({ description, inputSchema, execute }), { needs: crm });
}

Connection, SignIn and NeedsSignIn are exported from @chloejs/core.