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 that fails almost always throws instead, with Internal tool error and a short id but not the reason: a path with no tool, a wrong input, a missing file and an expired sign-in look the same. Check the path and the input with tools.describe.tool first, and 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.

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, which nothing can ask for yet:, each such call with its tool and exact input (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 an MCP server marks that way, does not run: 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, under Needs the user's approval. 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. Ask June for these steps in a chat on the web or in the app, where you approve each one on a card (how).

What June does not do

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