# Hubert MCP server

Hubert is a remote MCP server from team.blue. It lets AI assistants such as Claude and ChatGPT write and publish Instagram posts for anyone signed in to Hubert. MCP (Model Context Protocol) is the open standard these assistants use to call outside tools. No tool deletes published content, and every publishing call must carry an explicit confirmation flag.

- **Transport:** Streamable HTTP (stdio available for local clients)
- **Authentication:** OAuth 2.1
- **Endpoint:** `https://mcp.hubert.team.blue/central/mcp`
- **Status:** Pre-release. Not yet listed in the Claude or ChatGPT directories
- **Last updated:** 30 September 2026

> **This server is not generally available yet.** Tool names may still change. Everything below describes the current build.

---

## Quick start

Sign-in needs a Hubert account. Create one for free at [app.hubert.team.blue/en/hub/signup](https://app.hubert.team.blue/en/hub/signup). Without an account, tools return `ACCOUNT_SETUP_REQUIRED`.

`list_brands` and `search` answer without a token, so an agent can probe the server before the user signs in. Without a token, `search` shows only what needs no account.

### Claude

1. Open **Settings → Connectors → Add custom connector**.
2. Paste the server URL: `https://mcp.hubert.team.blue/central/mcp`
3. Complete the OAuth sign-in when prompted. You approve access once; the connection persists.
4. Ask for a post. See the [example prompts](#example-prompts).

### ChatGPT

1. Open **Settings → Connectors → Create**.
2. Paste the server URL: `https://mcp.hubert.team.blue/central/mcp`
3. Choose **OAuth** as the authentication method and complete sign-in.

### Any MCP client (manual configuration)

```json
{
  "mcpServers": {
    "hubert": {
      "type": "http",
      "url": "https://mcp.hubert.team.blue/central/mcp"
    }
  }
}
```

### Local / stdio

For clients that start the server as a local process:

```bash
MCP_TRANSPORT=stdio node dist/index.js
```

---

## How it works

Your assistant never talks to the social networks directly. It calls Hubert, which holds the user's connected accounts and forwards the action to the team.blue Hub API.

```
┌──────┐    ┌────────────┐    ┌─────────────┐    ┌─────────┐
│ You  │──▶ │ Claude /   │──▶ │ Hubert MCP  │──▶ │ Hub API │
│      │    │ ChatGPT    │    │ server      │    │         │
└──────┘    └────────────┘    └──────┬──────┘    └─────────┘
                                     │
                                OAuth 2.1
                            (you approve once)
```

The assistant sees only the tools listed below. It cannot reach the Hub API by any other route, and it cannot act for anyone who has not signed in.

---

## The posting flow

At `initialize`, Hubert returns instructions that set the order of the steps below. Several tools are stateful: if a step is skipped, the tool returns a recoverable error.

1. **Sign in.** With OAuth, this happens outside the chat. Never ask the user for an email address or a code. If a result says the account needs setting up, present the returned action and wait for the user to confirm they are done, then retry.
2. **Compose.** `ig_compose_start` starts the composer. Called with no topics, it returns four post ideas based on the user's business. Call it again with exactly one topic, either one of those ideas or the user's own wording, to generate the caption and image. The image is a stock photo (Unsplash or Pexels), never AI-generated. Clients that support MCP Apps can also show an interactive composer, but do not assume it rendered. Do not tell the user to open a separate Hubert page unless a tool returns an explicit URL.
3. **Connect, if needed.** Call `metricool_social_status` to check. If Instagram is not connected, call `metricool_social_connect` and present the returned link with the label "Connect Instagram". Poll `metricool_social_status` and continue automatically once it succeeds.
4. **Confirm.** Show the final caption, image, post type and publication time. **Never call a publishing tool before the user explicitly approves.**
5. **Publish and verify.** Call the publishing tool with `confirmed: true`, then check `metricool_scheduled_posts_list` to report the real state.

**There are two separate setup gates.** `ACCOUNT_SETUP_REQUIRED` means the user has no provisioned team.blue business account. Nothing can proceed until the user completes the returned set-up action. A `metricool_social_status` result with no connected account means the business exists but has no Instagram account linked; `metricool_social_connect` fixes that. Resolving one gate does not resolve the other.

Two more rules:

- Keep the conversation user-facing. Do not mention internal systems or tool names unless the user asks for implementation details.
- Only describe Hubert as unavailable on `SERVICE_UNAVAILABLE` or a genuine network/5xx result. **Never infer an outage from a 404.**

---

## Authentication

Hubert authenticates with **OAuth 2.1**. The server is an OAuth-protected resource; your client discovers the authorization server from the standard protected-resource metadata document and runs the authorization-code flow with PKCE.

| Property | Value |
|---|---|
| Protocol | OAuth 2.1 |
| Scopes | `openid`, `email`, `profile` |
| Assurance level | `urn:tblue:cognito:signed-in` |
| Token algorithms | RS256, ES256, verified against the issuer's JWKS |
| Claims validated | `iss`, `aud`, `exp`, `nbf`, and optionally `client_id` |

Every brand action except `health` requires a valid bearer token. Calling an authenticated one without a token returns `SIGN_IN_REQUIRED`. On the Central URL, `list_brands` and `search` also answer without a token.

Identity resolves from the token subject. There is no per-brand allow-list on the server. Anyone who completes sign-in can use it, and can act only on what their own account owns.

> **The `hubert_auth_*` tools are not part of the OAuth flow.** They implement a six-digit email OTP used in local development, and their sessions live in the server process's memory only, so a restart signs everyone out. They are not present on the hosted server. Production clients should ignore them.

There is no auth-start tool on the OAuth path. If a session expires mid-conversation, the client handles re-authentication: tell the user and let the connector sign them in again. The `expired` status and the auth-start guidance in the status table apply to the local OTP flow only.

---

## Tools

The hosted URL exposes five Central tools (see [The Central server](#the-central-server)). The tools below are the brand actions that `search` returns and the three `execute_*` tools run.

Tools marked **worker** are called by Hubert's embedded composer widget. They are listed for completeness; the model should not call them.

### Composing

| Tool | What it does | Auth | Write |
|---|---|---|---|
| `ig_compose_start` | Starts the composer. With no `topics`, returns four post ideas based on the user's business; call again with exactly one topic to generate caption and image. Optional `topics` (1–5 strings, ≤400 chars each), `language` (BCP-47), `business_type` (≤120 chars), `country` (≤80 chars). Topics are free text: the user's own wording is valid and does not have to match a suggestion. | Required | Yes |
| `ig_caption_edit` | Applies one targeted change the user asked for to a caption they have already seen, such as adding a call to action or changing a day. Every untouched sentence comes back word for word and the image is unchanged. Not for a whole-caption rewrite such as shortening or a tone change. Requires the exact current `caption` and an `instruction` (≤200 chars). | Required | Yes |
| `ig_caption_regenerate` | **Worker.** Generates a fresh caption for the current post. | Required | Yes |
| `ig_image_store` | **Worker.** Stores a base64 image and returns a public URL. Requires `mimeType`, `dataBase64`. | Required | Yes |
| `ig_image_import` | **Worker.** Downloads an image from `imageUrl` and persists it through the media pipeline. | Required | Yes |

The composer returns the generated caption and image for review. Those are the values you pass as `text` and `image_url` when publishing. Read them from the tool result instead of hard-coding field names, because the response shape may still change.

### Publishing

The hosted server publishes to Instagram only. Facebook is used only to connect an Instagram Business account; there is no Facebook post tool.

| Tool | What it does | Auth | Write |
|---|---|---|---|
| `metricool_post_create` | Publishes an approved Instagram image post, now or at `publication_date`. Requires `confirmed: true`, `text`, `image_url`. Optional `publication_date` (`dateTime` and `timezone`; defaults to about 30 seconds from now), `instagram_type` (`POST` \| `REEL` \| `STORY`; only image posts are supported end to end), `overlay`. | Required | Yes, open-world |

### Accounts

| Tool | What it does | Auth | Write |
|---|---|---|---|
| `metricool_social_connect` | Prepares the Instagram connection and returns one link for the user to approve. | Required | Yes |
| `metricool_social_status` | Checks whether Instagram is connected. Poll this after presenting the link. | Required | No |
| `metricool_facebook_login_accounts` | Lists the accounts available after approval. Ask which one if there is more than one. | Required | No |
| `metricool_facebook_login_finalize` | Finishes connecting the selected account. Omit `page_id` only when exactly one is available. | Required | Yes |
| `metricool_social_disconnect` | Disconnects Instagram. | Required | **Destructive** |
| `metricool_social_resolve_duplicate` | Moves an Instagram account already connected elsewhere to this Hubert account. | Required | **Destructive** |

### Reading and utility

| Tool | What it does | Auth | Write |
|---|---|---|---|
| `metricool_scheduled_posts_list` | Lists recent scheduled posts and their state: queued, processing, published or failed. Use after publishing. | Required | No |
| `metricool_instagram_posts_list` | Lists recent published posts. Use to verify a post is live. | Required | No |
| `health` | Liveness check. Report only available or unavailable. | **None** | No |

Hubert publishes these groupings as MCP annotations: `writeTools` (everything that changes state), `destructiveTools` (the two above), and `openWorldTools` (the publishing tool, which reaches a system outside team.blue).

---

## Response envelope

Every tool returns a `structuredContent` object with a `status`. The calling model should branch on that status, using the table below.

```json
{
  "structuredContent": {
    "status": "action_required",
    "code": "ACCOUNT_SETUP_REQUIRED",
    "action": { "type": "open_url", "label": "Connect Instagram", "url": "https://..." },
    "resume_original_request": true
  }
}
```

Fields you may also receive: `next_tool` and `retry_tool` name the exact tool to call next; `retryable` is a boolean on error envelopes.

Neither field is always present. When a status tells you to retry, use `retry_tool` if it is there, otherwise `next_tool`, and if neither is given re-issue the original call with the same arguments.

| `status` | Meaning | What to do next |
|---|---|---|
| `action_required` | The user must do something outside the conversation, usually approve a connection. | Present the `action` label and URL, wait for the user, then call `retry_tool` and resume. |
| `in_flight` | An identical publish is already running from an earlier request. | **Do not publish again.** Call `metricool_scheduled_posts_list` to confirm. |
| `scheduled` | The post was accepted and queued. | Call `metricool_scheduled_posts_list` to report the real state. |
| `unknown` | The post may have been accepted but no reference came back (`PUBLICATION_REFERENCE_MISSING`). | Check scheduled posts before doing anything else. Retrying blindly risks a double post. |
| `resolved` | A duplicate account link was resolved. | Retry the connect flow. |
| `error` | Generic failure. Carries a `code` and `retryable`. | Surface the message; retry only if `retryable` is true. |
| `retry_later` | Rate limited. | Wait, then call `retry_tool`. |
| `pending` | Waiting on a user action. | Wait; do not re-issue. |
| `authenticated` | Already signed in. | **Do not ask for a code.** Continue the original request now. |
| `anonymous` | Signed out, no session. | Start the sign-in flow. |
| `expired` | The session or token expired. | Local OTP only: restart that flow's auth-start tool. On the OAuth path, the client handles re-authentication. |
| `conflict` | A local session is already signed in as someone else. | The user must sign out first. Not retryable. |
| `code_required` | A six-digit code is outstanding (local OTP only). | Ask for the code, call `next_tool`. |
| `email_required` | The composer needs an email first (local OTP only). | Call `next_tool`. |

Treat an unrecognised `status` as terminal and report it to the user.

> `step` is a different field from `status`. It carries composer UI state (`topics`, `preview`, `caption`, `business_context`) and is not part of recovery logic. Do not branch on it.

---

## Errors

| Code | HTTP | Retryable |
|---|---|---|
| `SIGN_IN_REQUIRED` | 401 | Yes, after the user signs in |
| `ACCESS_DENIED` | 403 | No |
| `BUSINESS_SETUP_REQUIRED` | 404 | No |
| `ACCOUNT_SETUP_REQUIRED` | 404 | Yes, after the user completes the `open_url` action |
| `VERIFICATION_EXPIRED` | 404 | Yes, restart the flow |
| `SERVICE_UNAVAILABLE` | 5xx | Yes |
| `REQUEST_REJECTED` | other 4xx | No |
| `RATE_LIMITED` | 429 | Yes, after the window resets |
| `CODE_NOT_ACCEPTED` | 400 / 401 / 409 | Yes, ask for the code again |
| `LOCAL_SESSION_IN_USE` | n/a | No |
| `INVALID_BACKEND_RESPONSE` | n/a | Yes |
| `PUBLICATION_REFERENCE_MISSING` | n/a | No. Check scheduled posts first |
| `CENTRAL_SEARCH_FAILED` | n/a | Yes |
| `BRAND_REQUEST_FAILED` | n/a | Yes |

Transport-level: a non-POST request to the MCP endpoint returns JSON-RPC error `-32000`, `"Method not allowed. Use POST for MCP requests."`

A 404 means a specific resource is missing. It does not mean Hubert is down.

---

## Rate limits

**60 requests per 60 seconds**, counted in fixed windows.

Requests are counted per authenticated subject once a bearer token has been verified, and per IP address for anonymous calls. Exceeding the limit returns HTTP 429, surfaced as `RATE_LIMITED` with status `retry_later`.

The limiter runs in memory in each container and is not shared between instances. With several instances running, each one allows 60 requests per window.

---

## Safety and limits

**No tool deletes published content.** The server has no delete tool, so the worst a successful prompt injection can do is publish an unwanted post. The limit comes from which tools exist, so no prompt can lift it.

Two tools are annotated destructive: `metricool_social_disconnect` and `metricool_social_resolve_duplicate`. They disconnect or move an Instagram account. Neither of them removes published content.

Publishing tools require `confirmed: true`. Set it only after the user has seen and approved the exact text and image.

What Hubert cannot do:

- Delete published content
- Publish anything without `confirmed: true`
- Reach accounts the user has not connected
- Read private messages

---

## Example prompts

- "Post something about our new opening hours to Instagram."
- "Which social accounts do I have connected?"
- "Write an Instagram post announcing we're hiring a pastry chef."
- "What have I got scheduled this week?"
- "Give me a few post ideas for this week."

---

## The Central server

Hubert is served through Central, team.blue's portfolio surface, at `https://mcp.hubert.team.blue/central/mcp`. Central routes a request to the right team.blue brand without loading every brand's tools into context. Hubert is currently the only onboarded brand. It exposes five tools:

- `list_brands`: first step. Lists the onboarded brands, your access state and each brand's entry points.
- `search`: second step. Matches the user's request to a brand action and returns the routing values, input schemas and safety flags.
- `execute_read`: runs a brand action that only returns information, such as a status check or a listing.
- `execute_write`: runs a brand action that creates or changes something, from drafting a caption to publishing a post. Publishing needs the user's explicit approval (`confirmed: true`).
- `execute_destructive`: runs a brand action that removes or disconnects something. Ask the user first and pass `confirmed: true` only after approval.

---

## Support

- Email: [hubert@team.blue](mailto:hubert@team.blue)
- Site: [hubert.team.blue](https://hubert.team.blue/en/)
