Write June MCP programs
How a June MCP program finds and calls your integrations' tools, three worked examples, what comes back, and its limits.
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.
toolsreaches your integrations. It is the only way out:fetchis switched off, and there are no files, packages or commands.console.log,console.info,console.warn,console.errorandconsole.debugwrite 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:
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:
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:
await tools.<integration>.<owner>.<connection>.<tool>(input)<integration>is the name search reports, such asjune_github.<owner>isuserfor a connection you signed in to, andorgfor 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 aswork. 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:
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
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
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.
Read a file from a GitHub repository
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 peremit(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). - 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,passwordorapi_key(alsoapikey), or ending in_token,_secret,_passwordor_api_key, such asaccess_tokenorclient_secret, comes back as[REDACTED], whatever it held. So doBearervalues, and pairs such astoken=…orpassword=…in text. Other spellings, such asaccessToken, 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.
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) |
| 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).
What June does not do
- It does not keep anything between programs: each call to
executestarts 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.
Set up your client
Add June MCP to Claude Code, Codex, Cursor, OpenCode or any client that adds remote MCP servers with a sign-in, then sign in with your June account.
Security and privacy
Who sees your chats, where your connections' passwords and tokens go, what reaches the model, and what share links expose.