# Write June MCP programs

> How a June MCP program finds and calls your integrations' tools, three worked examples, what comes back, and its limits.

Source: https://www.dearjune.ai/docs/june-mcp/programs

The `execute` tool runs one short program against your integrations and hands its result to your client. Your client usually writes the program for you; this page is for when you write one yourself, or want to know why one failed.

## How a program runs

A program is TypeScript (or plain JavaScript) that runs on June's side, in its own small runtime. It is the body of an async function: `await` works at the top, and `return` gives the result.

- `tools` reaches your integrations. It is the only way out: `fetch` is switched off, and there are no files, packages or commands.
- `console.log`, `console.info`, `console.warn`, `console.error` and `console.debug` write lines that come back with the result.
- It is not June's sandbox: your **Environment** packages and the sandbox's files are not there.

## Find a tool

Ask `tools.search` in plain words. It ranks the tools of every connection you have and returns the best 12:

```ts
const found = await tools.search({ query: "search mail" });
return found.items.map((tool) => tool.path);
```

Each item has a `path`, the tool's `name`, its `description`, its `integration` and a `score`. Pass `limit` for more, and `offset` with the `nextOffset` of the last answer for the next page.

To list every tool of one integration, pass its name as both `namespace` and `query`: `tools.search({ namespace: "<integration>", query: "<integration>" })`. With an empty query, search returns nothing.

Before you call a tool, read what it takes:

```ts
return await tools.describe.tool({ path: "june_github.user.default.get_file" });
```

The answer has the input as a TypeScript type (`inputTypeScript`) and the output's type. A path that does not exist answers with suggestions instead.

## Call a tool

A tool's path is its address under `tools`:

```ts
await tools.<integration>.<owner>.<connection>.<tool>(input)
```

- `<integration>` is the name search reports, such as `june_github`.
- `<owner>` is `user` for a connection you signed in to, and `org` for an MCP server you added that needs no sign-in. Both are yours: June MCP never reaches an organization's connections, whatever the address says.
- `<connection>` is the connection's name: `default`, unless you gave it one such as `work`. With two accounts on one service, each has its own address.

A call that works answers `{ ok: true, data }`, with the tool's answer in `data`. A tool's type also allows `{ ok: false, error: { code, message } }`, so check `ok` before you read `data`:

```ts
const file = await tools.june_github.user.default.get_file({ owner: "acme", repo: "website", path: "README.md" });
if (!file.ok) return file.error;
return file.data.content;
```

A call to one of June's own integrations (the `june_` ones) that fails answers `{ ok: false, error }` too, with the reason in `error.message` and a short id at its end. A wrong input names the parameter: `inspect_url: unknown property "inspectionUrl"; did you mean "url"?`. A provider's refusal keeps its words, and a path with no tool answers `tool_not_found` with the closest names. Check the input with `tools.describe.tool` when the message is not enough.

A call to an MCP server you added can still throw `Internal tool error` and a short id, without the reason. Catch errors where one failing call should not end the program.

## Examples

Each example uses the `default` connection; use the address `tools.search` gives you for yours. Replace the sample account, repository or search terms before running it. Start with a read to check that you are using the intended account.

### Find unread mail from someone

```ts
const found = await tools.june_email.user.default.search_messages({
  from: "accountant@acmestudio.com",
  unread: true,
  after: "2026-10-01",
  limit: 20,
});
if (!found.ok) return found.error;
return found.data.messages.map(({ date, from, subject }) => ({ date, from, subject }));
```

`search_messages` also filters by `to`, `subject`, `text` and `before`, and returns at most 50 summaries at a time, with a `nextCursor` to pass as `cursor` for the next page. To read one message in full, pass its `id` to `get_message`. Outlook mail has the same tools under the Microsoft 365 integration, `june_microsoft`.

### Read a Google Drive document

```ts
const drive = tools.june_google_drive.user.default;
const found = await drive.search_files({ query: "Q3 plan", limit: 5 });
if (!found.ok) return found.error;
const [file] = found.data.files;
if (!file) return "No file matches.";

const read = await drive.read_file({ fileId: file.id, maxCharacters: 20000 });
if (!read.ok) return read.error;
return {
  name: file.name,
  link: file.webViewLink,
  truncated: read.data.truncated,
  text: read.data.content,
};
```

`truncated` says whether the document went on past `maxCharacters`. A Google Sheet is read with `read_sheet` instead, one range of rows at a time (`nextRange` names the rows after it); see [Google Drive](/docs/integrations/google-drive).

### Read a file from a GitHub repository

```ts
const github = tools.june_github.user.default;
const results: Record<string, unknown> = {};
for (const repo of ["website", "api"]) {
  try {
    const details = await github.get_repository({ owner: "acme", repo });
    if (!details.ok) {
      results[repo] = details.error;
      continue;
    }
    const readme = await github.get_file({ owner: "acme", repo, path: "README.md" });
    if (!readme.ok) {
      results[repo] = readme.error;
      continue;
    }
    results[repo] = {
      branch: details.data.defaultBranch,
      readme: readme.data.content.slice(0, 2000),
    };
  } catch (error) {
    results[repo] = String(error);
  }
}
return results;
```

`get_file` reads one text file of up to 100 KB, from the default branch unless you pass `ref`, a branch, tag or commit. A client reaches GitHub only through these reads: there is no listing of repositories, issues or pull requests.

## What comes back

- **The result:** what the program returns. A string comes back as it is; anything else as indented JSON.
- **What it emitted:** after the result, under `Emitted:`, one line per `emit(value)`: a string as it is, anything else as JSON, and an image or a file by its type, such as `[image image/png]`.
- **Steps that need your approval:** next, under `Needs the user's approval; nothing has run.`, each such call with its tool, its exact input and the link to approve it, such as `→ approve at https://www.dearjune.ai/approve/…`. A call a rule blocks says so instead, with no link (see [below](#missing-connections-and-approvals)).
- **The logs:** last, under `Logs:`, each line with its level in front, such as `[log] 12 messages`.
- **An error:** if the program throws or stops, the client gets the error, with the logs below it, and no result.
- **Credentials stay out:** a text field named `token`, `secret`, `password` or `api_key` (also `apikey`), or ending in `_token`, `_secret`, `_password` or `_api_key`, such as `access_token` or `client_secret`, comes back as `[REDACTED]`, whatever it held. So do `Bearer` values, and pairs such as `token=…` or `password=…` in text. Other spellings, such as `accessToken`, are not caught.

## Limits

| Limit                    | Value                                                                 | Beyond it                                                                                              |
| :----------------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- |
| Program                  | 20,000 characters                                                     | the call is refused before it runs                                                                     |
| Run time                 | 60 seconds, counting the time spent waiting for tools                 | the program stops and the call answers with a timeout error                                            |
| Memory                   | 64 MB                                                                 | the program stops with an error                                                                        |
| Result                   | 64 KB, measured as JSON                                               | the result is cut at 64 KB and ends in `...[TRUNCATED]`; it then arrives as text, no longer valid JSON |
| Logs                     | the first 200 lines, each up to 4,000 characters, 64,000 bytes in all | later lines are dropped and a longer line is cut                                                       |
| Emitted                  | the same as logs                                                      | the same as logs                                                                                       |
| Steps that need approval | the first 20, 64 KB in all                                            | the rest are counted on one line, such as `- 5 more, not listed`                                       |

Return only the fields the client needs, such as subjects rather than whole messages. For a long file, cut the text in the program or ask the tool for less: Google Drive's `read_file` takes `maxCharacters` (50,000 by default).

## Missing connections and approvals

An integration you have not connected has no tools to find. `skills` names the MCP servers you added on the **Integrations** page but have not signed in to, and the client can give you a June link to [connect an integration](/docs/june-mcp#when-a-connection-is-missing).

A step that needs your approval, such as creating a Microsoft 365 calendar event, sending a Teams message or a tool of an MCP server you added that changes something, does not run unless a rule of yours allows it: the call throws an error that names the tool and says it needs your approval and did not run, and the answer lists the call, with its exact input and its approval link, under `Needs the user's approval`. An uncaught error ends the program. Catch it only if independent work should continue; do not continue with a step that depends on the change being approved.

The client gives you the link and stops. Once you have approved or denied the step on its approval page, `approval` with the step's id, the last part of the link, tells the client how it ended:

| The step                    | `approval` answers                                                                                                                                                                                                             |
| :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| still waiting for you       | that it waits, with the link again; it waits up to 50 seconds first (`waitSeconds`, at most 300; `0` reads once; a client that waits more than 60 seconds may need a longer tool timeout, see [Codex](/docs/june-mcp/clients)) |
| approved                    | that it ran once, with what the call returned (the same as `execute` would have returned, such as an Apify run id), or that it ran and failed, or that nothing was changed                                                     |
| denied                      | that you denied it and nothing ran, with your reason if you gave one                                                                                                                                                           |
| not decided within 24 hours | that it expired and nothing ran                                                                                                                                                                                                |
| not yours, or no such step  | `No such approval.`                                                                                                                                                                                                            |

`approval` never runs the step: approving it on the page does, once. Running the same program again asks again, with a new link, and runs the rest of the program again, so the client does not. An MCP server's tool that asks you a question while it runs is told no; what it does then is up to the server, and it is not listed ([more on approvals](/docs/june-mcp#when-a-step-needs-your-approval)).

## What June does not do

- It does not keep anything between programs: each call to `execute` starts fresh.

## Upload a file for an email draft

An agent can call `prepare_file_upload` with a filename, media type and size
in bytes. It receives a private upload URL valid for 15 minutes. Outside
`execute`, POST the file's raw bytes to that URL with the returned
`Content-Type` header. The response returns an `assetId`; use it in the
mailbox's `create_draft` attachment list: `attachments: [{ assetId }]`.

Each upload accepts one file up to 10 MB. Supported types include PDF, plain
text, CSV, Markdown, JSON, Office documents and PNG, JPEG, GIF or WebP images.
The URL works once; a failed upload needs a new URL. Uploaded files belong
to your personal account. Files no June message or saved draft references
are removed after seven days without use. Sending a saved email draft
still follows June's approval rules.