# Custom connectors

A **custom connector** is a remote MCP server —the one for your billing system, your ERP, your
inventory, or a third party's such as Notion or Shopify— registered in your workspace. Meteor asks
it for its list of actions, you choose which ones stay on, and your Mets use them like any other
skill.

It is **the same connector as the Connections screen**: what you create over the API shows up
there, and what someone creates there you see over the API.

## Before you start

- The key needs `integrations:read` to read and `integrations:manage` to create and change.
- The key has to **belong to a workspace administrator**, just like the screen. With the key of
  someone who is not one, you get `403` even if it carries the scopes.
- The server has to be on **HTTPS** and reachable from the internet. Addresses on internal
  networks are rejected.

## Create one by its URL

```ts
import Met from '@meteor.ia/sdk';

const met = new Met(process.env.METEOR_API_KEY!);

const connector = await met.customConnectors.create({
  name: 'Facturación',
  url: 'https://mcp.mi-erp.com/mcp',
  auth_mode: 'bearer', // 'none' | 'api_key' | 'bearer' | 'oauth'
  secret: process.env.ERP_TOKEN!,
});

console.log(connector.id, connector.status); // '48211' 'connected'
for (const a of connector.actions) console.log(a.enabled ? '●' : '○', a.name, '→', a.key);
```

Before saving, Meteor asks the server for its list of actions **with your credential**. If the
server does not answer or rejects the key, you get a `400` and nothing is created: a connector that
exists is one that answered.

With `auth_mode: 'api_key'` the key goes in a header; `X-API-Key` by default, or the one you set in
`header`. With `'none'` no credential is sent.

Each action has two names: `name` is the server's and `key` is the one a Met sees
(`mcp_c_<slug>_<action>`). The `c_<slug>` prefix comes from the connector's name and does not change
afterwards.

## From a template

For third parties' official MCP servers you do not need to type the URL:

```ts
const templates = await met.customConnectors.templates();
// [{ id: 'notion', name: 'Notion', auth_mode: 'oauth', asks_for: 'none', connector: null }, …]

await met.customConnectors.create({ template: 'freshdesk', subdomain: 'miempresa', secret: process.env.FRESHDESK_KEY! });
```

`asks_for` tells you whether the template asks for an extra value: `subdomain` or `url` (your own
instance). If the workspace already created one, `connector` says which. In templates, whatever
deletes, cancels or refunds money starts **switched off**.

## Connectors with OAuth

An `oauth` connector is created over the API, but **it is authorized in Met**: consent needs a
browser, and a key cannot do that. The response tells you so:

```ts
const c = await met.customConnectors.create({ name: 'Mi CRM', url: 'https://mcp.mi-crm.com/mcp', auth_mode: 'oauth' });
if (c.requires_authorization) {
  console.log('Ask an administrator to authorize it in Met: Conexiones → Mi CRM → Autorizar.');
}
```

If the server does not allow automatic app registration, create your own app in that service and
send its `client_id` (and `client_secret`, if it has one).

## Switch actions on and off

```ts
await met.customConnectors.setDisabledActions(connector.id, ['borrar_factura', 'anular_factura']);
```

You send the **complete** list of switched-off actions; whatever is not on the list stays on. Each
connector has room for the description of its actions (`space`): what does not fit is imported
switched off (`no_room: true`). Switching off is always allowed; switching on one that does not fit
answers `400 CUSTOM_CONNECTOR_NO_ROOM`, and you have to switch another one off first.

## Test and change the key

```ts
// Lists the actions again with the saved key and updates the status.
const tested = await met.customConnectors.test(connector.id);

// The new key is tested before it is saved: if the server rejects it, the previous one stays.
await met.customConnectors.updateCredentials(connector.id, { auth_mode: 'bearer', secret: newToken });
```

`status` uses the same vocabulary as the screen: `connected`, `needs_reauth` (the server rejected
the credential), `error` or `disconnected`.

## The credential cannot be read

The key or token is written and **never returned**: it is stored encrypted, and it only travels to
the URL you created the connector with. `credential.configured` tells you whether there is one, and
`credential.updated_at` when it was changed. If you need the key again, keep it on your side too.

The URL's query string does not come out either: some servers carry a key there.

## The contact's phone

When a Met uses an action while handling a conversation, Meteor adds the
**`X-Meteor-Contact-Phone`** header to that call with the phone of the conversation's contact,
exactly as it is on their record (for example `+573001112233`). A flow node that runs inside a
conversation does the same.

- **Meteor sets it, not the model.** An action's arguments (a `telefono`, a `documento`) are written
  by the Met, and a person can ask it to use someone else's. The header cannot be changed that way:
  it comes from the contact the conversation is with. If your server acts on a person's data, **use
  the header and prefer it over the argument**; if both arrive and they do not match, reject the
  call.
- **It only arrives when there is a contact with a phone.** Without a conversation
  (`integrations.execute`, a flow with no contact) or with a contact with no phone, the header does
  not arrive; it never arrives empty. Treat its absence as "I do not know who this is", not as
  permission.
- **Nobody else writes it.** The credential cannot use that header name (`400
  CUSTOM_CONNECTOR_HEADER_INVALID`) and, if it carried it, Meteor replaces it.
- **The customer cannot change a phone that already exists.** In a conversation with a customer,
  the Met saves the phone only if the contact does not have one yet (for example, in your website
  chat); if it already has one, it does not overwrite it even when asked to. The channel identifier
  is never changed. Your team can change both (the inbox, the API or a flow).
- **Outside WhatsApp, the phone was given by the person.** In the web chat, Messenger or Instagram,
  the phone the Met saved is the one the person typed: it is not verified. If your server needs
  certainty, verify it on its side (with a code, for example).

## Use its actions from your code

The connector's `id` is also its `key` in integrations, so you can run an action without going
through a Met (scope `integrations:execute`):

```ts
await met.integrations.execute(connector.id, 'crear_factura', { cliente: 'Panadería Luz', total: 120000 });
```

## In Python

```python
from meteor_ia import Met

met = Met("met_live_…")
c = met.custom_connectors.create(name="Facturación", url="https://mcp.mi-erp.com/mcp", auth_mode="bearer", secret=token)
met.custom_connectors.set_disabled_actions(c["id"], ["borrar_factura"])
```

## What about `met.integrations.register`?

It keeps working the same. A custom connector is the recommended way to bring your MCP server: the
credential is encrypted, you choose which actions are used, and the team sees it in Connections.
