# Upfirst MCP Server

Upfirst's MCP server lets an AI assistant, like Claude, Cursor, or Codex,
connect to your Upfirst account through the Model Context Protocol (MCP).

Once it's connected you can work by asking instead of clicking. Pull up last
week's calls, read a transcript, fix an answer your receptionist got wrong, add
a rule for transferring callers, or set up a whole new receptionist from a
description of your business.

Reference page: https://upfirst.ai/docs/mcp

## Connecting

There is nothing to install. Point your client at `https://mcp.upfirst.ai` and
it will walk you through signing in to Upfirst the first time it connects.
Authorization is a standard OAuth 2.1 sign-in, so there are no API keys to copy
or store.

The server runs over streamable HTTP and gives your assistant 17 tools, which
can both read your account and change it.

### Claude (desktop app and claude.ai)

Upfirst connects as a custom connector. Paste this prompt into Claude and it
will walk you through the setup one step at a time:

```
Help me connect Upfirst to Claude as a custom connector, so I
can review my calls and manage my AI receptionist from this chat.
Walk me through the exact clicks, one step at a time, and wait for
me at each one.

Open claude.ai/customize/connectors, click + Add custom connector,
and paste in these values:

  Name:                   Upfirst
  Remote MCP server URL:  https://mcp.upfirst.ai
  Advanced settings:      leave Client ID and Client Secret empty

Then click Add, click Connect, sign in to Upfirst, and approve
access.

If what I see doesn't match what you described, troubleshoot it
with me. Once it works, run get_account_overview and tell me what
you can see, then suggest three useful things I could ask you
about my calls.
```

To click through it yourself instead: open **Customize > Connectors**, click
**+**, then **Add custom connector**. Name it `Upfirst`, paste
`https://mcp.upfirst.ai` as the remote MCP server URL, then click **Add**,
click **Connect**, sign in to Upfirst, and approve access. Leave the advanced
Client ID and Client Secret fields empty.

On Team and Enterprise plans an owner adds the connector once under
Organization settings, and everyone else clicks Connect.

### Claude Code

```
claude mcp add --transport http upfirst https://mcp.upfirst.ai
```

That adds it to the current project. Add `--scope user` to make it available
everywhere. Then run `/mcp` inside Claude Code and sign in.

### Cursor

`~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "upfirst": {
      "url": "https://mcp.upfirst.ai"
    }
  }
}
```

Use `.cursor/mcp.json` in a project instead to scope it to that project.
Cursor prompts you to sign in the first time it calls the server.

### VS Code

`.vscode/mcp.json`:

```json
{
  "servers": {
    "upfirst": {
      "type": "http",
      "url": "https://mcp.upfirst.ai"
    }
  }
}
```

Start the server from the play button in the config file, then use it from
Copilot's agent mode. Sign in when prompted.

### Codex

`~/.codex/config.toml`:

```toml
[mcp_servers.upfirst]
url = "https://mcp.upfirst.ai"
```

Then run `codex mcp login upfirst` to authorize.

However you connect, the first call opens Upfirst's sign-in page. You approve
access once, and the connection stays bound to your organization from then on.

## Conventions

A few rules hold across every tool. Each one carries a tag for what it does to
your data:

- **Read**: fetches data; never changes anything.
- **Write**: creates or updates a record.
- **Delete**: permanently removes a record. There is no undo.

**Ids come from list tools.** Agent ids come from `list_agents`, skill ids from
`list_agent_skills`, knowledge ids from `get_agent_knowledge`, and call ids
from `list_calls`. Ids are digit strings. Skill tools take the id as
`skillId`; knowledge tools take it as `id`.

**Paging.** List tools take `offset` and `limit` and return a `totalCount`,
so the page is always drawn from the same filtered set.

**Timezones.** Bare dates (`YYYY-MM-DD`) and weekly schedules are interpreted
in the business's timezone. Pass a full ISO 8601 datetime when you need an
exact instant.

**Deletes are permanent.** There is no restore over this connection. A deleted
skill or knowledge entry is gone, and the agent stops using it within minutes.

**Some settings are dashboard-only.** Voice, timezone, and language; scheduling
and webhook skills; deleting a transfer skill; and importing website knowledge
are managed in the Upfirst dashboard, not over MCP.

## Account & agents

Orient yourself, then read or update an individual AI receptionist.

### get_account_overview (Read)

Start here. A compact snapshot of the whole account: the business name, every
receptionist with its timezone, greeting, phone numbers, skills and knowledge,
and the number of calls handled in the last 30 days.

No parameters.

Returns: business name; agents (id, name, timezone, greeting, phone numbers,
skill and knowledge names); calls in the last 30 days (settled calls only; test
and archived calls are not counted).

### list_agents (Read)

List the organization's AI agents. Use a returned id with the agent-scoped
tools below.

No parameters.

Returns: agents, each with id and name.

### get_agent_by_id (Read)

Read one agent's full conversational settings and attached phone numbers.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Numeric agent id from `list_agents`. |

Returns: greeting and goodbye messages, voice tone, speech rate, hold music,
language, timezone, spam and toll-free blocking, and attached phone numbers.

### update_agent (Write)

Change an agent's conversational settings. Partial update: send only what
changes; at least one settable field is required.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent to update. |
| `greetingMessage` | string, optional | Opening message. |
| `goodbyeMessage` | string, optional | Closing message. |
| `voiceTone` | enum, optional | `friendly`, `professional` |
| `speechRate` | number, optional | `0.7`, `0.85`, `1`, `1.1`, `1.2` |
| `holdMusic` | enum, optional | `ringTone`, `gentleGuitar`, `marimba`, `softKeys` |
| `isSpamCallsBlocked` | boolean, optional | Block suspected spam calls. |
| `isTollFreeCallsBlocked` | boolean, optional | Block toll-free calls. |

Voice, timezone, and language are managed on the dashboard and can't be changed
here. The blocking flags apply to this agent; the dashboard sets them for all
agents at once.

Returns: the updated agent, in the same shape as `get_agent_by_id`.

## Skills

A skill is an action a receptionist can take on a call: texting the caller,
texting a scheduling link, or transferring the call. Each kind has its own
tools, so the fields you pass are always the ones that kind uses. Scheduling
and webhook skills are read-only here and managed on the dashboard.

### list_agent_skills (Read)

List the skills configured for an agent, including inactive ones by default.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent whose skills to list. |
| `llmTool` | enum, optional | Only skills of this kind: `sendSms`, `sendScheduleSms`, `transferCall`, `scheduleSlot`, `customWebhook`. |
| `includeInactive` | boolean, optional | Include switched-off skills. Default `true`. |

Returns: skills (id, name, kind, active flag, stored config, optional weekly
schedule, and for webhook skills a webhook summary).

A schedule is honored on calls for transfer skills only. Other kinds store one
but ignore it.

### create_sms_skill (Write)

Add a texting skill: an SMS the receptionist can send to a caller during a
call. `sendSms` texts the message as written. `sendScheduleSms` texts it
together with the organization's scheduling link.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent to add the skill to. |
| `llmTool` | enum, required | `sendSms`, `sendScheduleSms` |
| `name` | string, required | Short label, shown on the dashboard. |
| `message` | string, required | The SMS text the agent sends, up to 306 characters. |
| `instruction` | string, required | When the agent should send it during a call. |
| `isActive` | boolean, optional | On from the start. Default `true`. |

The message passes a content filter that rejects promotional or otherwise
restricted wording.

Returns: the created skill with its new id, in the same shape as a
`list_agent_skills` entry.

### update_sms_skill (Write)

Change a texting skill. Partial update: only the fields you send change, and at
least one settable field is required.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent that owns the skill. |
| `skillId` | string, required | Skill id from `list_agent_skills`. |
| `llmTool` | enum, optional | Switch between `sendSms` and `sendScheduleSms`. |
| `name` | string, optional | New label. |
| `message` | string, optional | New SMS text, up to 306 characters. |
| `instruction` | string, optional | New guidance on when to send it. |
| `isActive` | boolean, optional | Switch the skill on or off. |

Returns: the fields the update wrote.

### delete_sms_skill (Delete)

Permanently delete a texting skill. The agent stops sending that message
immediately.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent that owns the skill. |
| `skillId` | string, required | Skill id to delete. |

There is no way to restore a deleted skill. Getting it back means creating it
again from scratch.

Returns: `{ id, deleted: true }`.

### create_transfer_call_skill (Write)

Add a transfer skill: the rule that hands a live call to a person.
`condition` tells the agent when to transfer, `preTransferMessage` is what
it says to the caller first, and `destinations` are the numbers it dials in
order.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent to add the skill to. |
| `name` | string, required | Short label, shown on the dashboard. |
| `condition` | string, required | When to transfer, in plain language. |
| `preTransferMessage` | string, required | What the agent says before transferring. |
| `destinations` | array, required | One or more targets, tried in order, each `{ phoneNumber, label, phoneExtension }`. `phoneNumber` is required and must be E.164 (e.g. `+12025550123`). |
| `ringTimeoutSeconds` | number, optional | Ring time per destination, 5-60. Default `30`. |
| `noAnswerAction` | enum, optional | `endCall` (default), `returnToAgent` |
| `transferMethod` | enum, optional | `cold` (default) hands the caller over directly; `warm` briefs the destination first. |
| `transferCallerId` | enum, optional | Number the destination sees: `upfirstNumber` (default), `callerNumber`. |
| `recordingMode` | enum, optional | `agentOnly` (default) stops recording at the transfer; `fullCall` keeps recording after it. |
| `isActive` | boolean, optional | On from the start. Default `true`. |
| `schedule` | object, optional | Weekly hours the skill is offered, in the agent's timezone. Omit for always available. See Schedules. |

Every destination must be in the same country as one of the account's Upfirst
numbers. Omitted options default to the same values the dashboard uses, so a
skill created here behaves identically to one built in the UI.

Returns: the created skill with its new id, in the same shape as a
`list_agent_skills` entry.

### update_transfer_call_skill (Write)

Change a transfer skill. Partial update: only the fields you send change, and
at least one settable field is required.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent that owns the skill. |
| `skillId` | string, required | Skill id from `list_agent_skills`. |
| `destinations` | array, optional | Replaces the whole list. Send every number you want to keep. |
| `schedule` | object, optional | Replaces the stored hours. `null` clears the schedule, making the skill available around the clock. |
| Other create fields | optional | `name`, `condition`, `preTransferMessage`, `ringTimeoutSeconds`, `noAnswerAction`, `transferMethod`, `transferCallerId`, `recordingMode`, `isActive`. Same values as create. |

A skill's kind is fixed at creation. Passing the id of a scheduling or webhook
skill reads as not found.

Returns: the fields the update wrote.

### Deleting a transfer skill

There is no tool for this. Transfer skills are deleted on the Upfirst
dashboard. Over MCP you can switch one off instead: set `isActive: false` with
`update_transfer_call_skill`, and the agent stops offering the transfer while
the skill stays configured.

## Knowledge

A receptionist's knowledge is what it answers callers from. In the Upfirst
dashboard these entries live under Training. Each one is text you write, or
content imported from a website. An entry can be shared across the
organization, in which case editing or deleting it changes what every agent
answers. Writes retrain the receptionist automatically within minutes.

### get_agent_knowledge (Read)

Read an agent's knowledge base. Every entry is returned whole with its full
content, never a preview.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent whose knowledge to read. |
| `id` | string, optional | Return just this one entry. |
| `offset` | number, optional | Entries to skip. Default `0`. |
| `limit` | number, optional | Max entries, 1-100. Default `25`. |

Returns: entries (id, name, type text/website, active flag, full content,
source url, weekly schedule) plus `totalCount`.

### create_agent_knowledge (Write)

Add a text entry to a receptionist's training. New entries go to the top of the
list.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent to add knowledge to. |
| `name` | string, required | Display name of the entry. |
| `content` | string, required | Plain text, up to 250,000 characters. |
| `isActive` | boolean, optional | Active from the start. Default `true`. |
| `schedule` | object, optional | Restrict the entry to business hours. Omit for always-active. See Schedules. |

Returns: the created entry.

### update_agent_knowledge (Write)

Change an entry's name, active flag, content, or schedule. Partial update; at
least one settable field is required.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent that owns the entry. |
| `id` | string, required | Entry id from `get_agent_knowledge`. |
| `name`, `isActive` | optional | New name / active flag. |
| `content` | string, optional | New text, replacing the stored content entirely. Up to 250,000 characters. |
| `schedule` | object, optional | New schedule. `null` clears it, making the entry always available; omit to keep the stored one. |

Content is replaced, never appended. Read the entry with
`get_agent_knowledge` first and send back the full text you want it to have,
including whatever you are keeping. If the entry is shared across the
organization, the edit changes what every agent answering from it says.

Returns: the fields the update wrote.

### delete_agent_knowledge (Delete)

Permanently delete a knowledge entry.

| Parameter | Type | Description |
| --- | --- | --- |
| `agentId` | string, required | Agent that owns the entry. |
| `id` | string, required | Entry id to delete. |

There is no way to restore a deleted entry. If the entry is shared across the
organization, deleting it removes it for every agent that answers from it.

Returns: `{ id, deleted: true }`.

### Schedules

A schedule restricts a knowledge entry (or transfer skill) to business hours,
honored in the agent's business timezone. It's a per-weekday object; each day
is on or off with one or more time windows.

A scheduled entry is only in the receptionist's knowledge during its windows.
Outside them, it's as if the entry doesn't exist, so the receptionist never
answers from it at the wrong time.

That makes schedules a dependable way to handle time-specific facts. To make
open and closed hours foolproof, add one entry restricted to your open hours
that reads "We are currently open," and a second restricted to your closed
hours that reads "We are currently closed." Only one is ever active, so the
receptionist can't mix them up.

```json
{
  "days": {
    "monday": { "enabled": true,  "workingPeriods": [{ "from": "09:00", "to": "17:00" }] },
    "tuesday": { "enabled": true,  "workingPeriods": [{ "from": "09:00", "to": "17:00" }] },
    "sunday":  { "enabled": false, "workingPeriods": [] }
  }
}
```

## Calls

Read the business's call history, one call's details, and its transcript. Only
calls that have finished appear; a call shows up shortly after it ends.

### list_calls (Read)

List and filter call history, most recent first. Compact rows with no
transcripts or summaries (use the tools below for those).

| Parameter | Type | Description |
| --- | --- | --- |
| `statuses` | enum[], optional | Filter by outcome, each call has exactly one: `test`, `blocked`, `spam`, `hungUp`, `completed`. |
| `query` | string, optional | Free-text search over call summaries and transcripts. |
| `tags` | string[], optional | Match calls carrying any of these tags (by name or id). |
| `startDate` | date, optional | Bare `YYYY-MM-DD` = calendar day in the business timezone, or a full ISO datetime. |
| `endDate` | date, optional | As above; inclusive. |
| `archived` | boolean, optional | Return archived calls instead of active ones. Default `false`. |
| `offset`, `limit` | number, optional | Paging. `limit` is 1-100, default `25`. |

Returns: call rows (caller, time, duration, outcome, tags, linked contact,
transcript-turn count) plus `totalCount`.

### get_call_by_id (Read)

Full details of one call, everything except the transcript text and recording.

| Parameter | Type | Description |
| --- | --- | --- |
| `callId` | string, required | Numeric call id from `list_calls`. |

Returns: timing, outcome, caller and receptionist numbers, the AI-written
summary, captured data fields, the skills the agent used (with when each
fired), tags, your team's comments, and the transcript-turn count.

### get_call_transcript (Read)

The conversation text of one call as ordered turns, each stamped with an
`[mm:ss]` offset and its speaker.

| Parameter | Type | Description |
| --- | --- | --- |
| `callId` | string, required | Numeric call id from `list_calls`. |
| `offset`, `limit` | number, optional | Paging over turns. `limit` is 1-200, default `100`. A typical call fits in one response; page only when the note says more turns remain. |

Speakers are Agent (the AI receptionist), Caller (the person who dialed), and
Transferee (a human the call was handed to).

Returns: turns (offset, speaker, text) plus `totalCount`.

## FAQ

**How do I get Upfirst to start answering my calls?**

We give you a phone number. You can hand that number out and have people call
it directly, but most businesses forward calls to it from the line they already
use.

You choose how much to forward: every call, only the ones you miss, or,
depending on your phone, carrier, or VoIP system, only during certain hours.
The steps differ for every provider, so see
https://help.upfirst.ai/en/articles/10318231-forward-all-your-calls-to-upfirst
for yours.

**Do I need an API key?**

No. Authorization is a standard OAuth 2.1 sign-in. The first call opens
Upfirst's sign-in page, you approve access once, and there is nothing to copy,
paste, or store.

**Which AI assistants can I use this with?**

Any client that supports remote MCP servers over HTTP. The Connecting section
has setup for Claude, Claude Code, Cursor, VS Code, and Codex. For anything
else, point it at `https://mcp.upfirst.ai` as a streamable HTTP server and it
will handle the sign-in on the first call.

**What can my assistant reach?**

Only the organization you signed in as. Every tool is scoped to that
organization, and ids from any other one are never accessible. Within it, the
assistant can read calls and transcripts and change receptionist settings,
skills, and knowledge, so treat the connection the way you would treat being
signed in to the dashboard.

**Why doesn't a call I just took show up?**

Only finished calls appear, and a call shows up shortly after it ends. Calls in
progress are not available until they hang up. If a call is still missing,
check whether it was archived, since `list_calls` returns active calls unless
you pass `archived: true`.

**What can't I do over MCP?**

Voice, timezone, and language; scheduling and webhook skills; deleting a
transfer skill; and importing knowledge from a website are all managed in the
Upfirst dashboard. The tools say so where it applies.
