June logo
JuneDocumentation
June MCP

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.

  • 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:

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 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:

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 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).
  • 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

LimitValueBeyond it
Program20,000 charactersthe call is refused before it runs
Run time60 seconds, counting the time spent waiting for toolsthe program stops and the call answers with a timeout error
Memory64 MBthe program stops with an error
Result64 KB, measured as JSONthe result is cut at 64 KB and ends in ...[TRUNCATED]; it then arrives as text, no longer valid JSON
Logsthe first 200 lines, each up to 4,000 characters, 64,000 bytes in alllater lines are dropped and a longer line is cut
Emittedthe same as logsthe same as logs
Steps that need approvalthe first 20, 64 KB in allthe 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 stepapproval answers
still waiting for youthat 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)
approvedthat 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
deniedthat you denied it and nothing ran, with your reason if you gave one
not decided within 24 hoursthat it expired and nothing ran
not yours, or no such stepNo 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 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.