MCP servers
Connect your own MCP server to the support chat so the assistant can look things up and act for a signed-in customer, using your tools and your rules.
How it workslink
The chat assistant is an MCP client. You add your server's address to a chat project in the dashboard, NewInstance discovers its tools, and the assistant calls them for the customer in the conversation. The assistant can only do what your server exposes, and your server decides what each call is allowed to do.
- You publish an MCP server over https.
- You add it under Chat, Connected servers, in the dashboard. NewInstance reads your server's metadata and lists its tools. Tools found when you first connect are turned on; tools that appear later start off until you turn them on.
- When a customer asks for something, NewInstance calls your server with a short-lived signed token that names that customer.
- Your server verifies the token, authorizes the call in your backend and returns the result.
Tools are available only in chat sessions that your backend created with a server-only key (see Create chat sessions with a server-only key below). Everyone else gets the knowledge-only assistant.
Protected resource metadatalink
Publish OAuth Protected Resource Metadata (RFC 9728) so NewInstance can see which authorization server your tools trust. Serve it at /.well-known/oauth-protected-resource, and point to it from the WWW-Authenticate header (resource_metadata) of your 401 responses.
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://service.newinstance.cloud/chat-mcp"]
}resource must match your MCP server URL exactly. It becomes the aud of every token NewInstance sends you. Listing the NewInstance issuer is the recommended setup: nothing else is needed on your side. To use your own sign-in service instead, see Your own sign-in service below.
Verify NewInstance tokenslink
Every request from NewInstance carries Authorization: Bearer <token>. The token is an ES256 JWT access token (typ of at+jwt), valid for up to 5 minutes (never longer than the chat session itself), and a new one is minted for every tool call. Discovery tokens are not tied to a chat session. Verify all of the following before you do anything else:
| Check | Expected value |
|---|---|
typ header | at+jwt |
| Signature | ES256, against the keys at https://service.newinstance.cloud/chat-mcp/.well-known/jwks.json (pick the key by kid) |
iss | https://service.newinstance.cloud/chat-mcp |
aud | Your server's resource value |
exp | In the future |
client_id | The client ID of your connection, shown in the dashboard |
Checking client_id pins your server to the one NewInstance connection you set up, so a token for another connection is refused.
Node (jose)
import { createRemoteJWKSet, jwtVerify } from 'jose';
const jwks = createRemoteJWKSet(
new URL('https://service.newinstance.cloud/chat-mcp/.well-known/jwks.json'),
);
export async function verifyNewInstanceToken(authorizationHeader) {
const token = (authorizationHeader ?? '').replace(/^Bearer /, '');
const { payload } = await jwtVerify(token, jwks, {
issuer: 'https://service.newinstance.cloud/chat-mcp',
audience: 'https://mcp.example.com/mcp',
algorithms: ['ES256'],
typ: 'at+jwt',
requiredClaims: ['exp', 'sub', 'client_id'],
});
if (payload.client_id !== process.env.NEWINSTANCE_CLIENT_ID) {
throw new Error('Unknown client_id');
}
return payload;
}PHP (firebase/php-jwt, version ^6.10 or later, which supports EC P-256 keys in a JWKS)
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
function newInstanceKeys(): array {
static $keys = null;
if ($keys === null) {
$json = function_exists('apcu_fetch') ? apcu_fetch('newinstance_jwks') : false;
$fresh = $json === false;
if ($fresh) {
$json = file_get_contents('https://service.newinstance.cloud/chat-mcp/.well-known/jwks.json');
if ($json === false) {
throw new RuntimeException('Could not fetch the NewInstance keys');
}
}
$keys = JWK::parseKeySet(json_decode($json, true), 'ES256');
if ($fresh && function_exists('apcu_store')) {
apcu_store('newinstance_jwks', $json, 3600);
}
}
return $keys;
}
function verifyNewInstanceToken(?string $authorizationHeader): object {
$token = preg_replace('/^Bearer /', '', $authorizationHeader ?? '');
$header = json_decode(JWT::urlsafeB64Decode(explode('.', $token)[0]), true);
if (($header['typ'] ?? '') !== 'at+jwt') {
throw new RuntimeException('Wrong token type');
}
$claims = JWT::decode($token, newInstanceKeys());
if ($claims->iss !== 'https://service.newinstance.cloud/chat-mcp') {
throw new RuntimeException('Wrong issuer');
}
if (!in_array('https://mcp.example.com/mcp', (array) $claims->aud, true)) {
throw new RuntimeException('Wrong audience');
}
if ($claims->client_id !== getenv('NEWINSTANCE_CLIENT_ID')) {
throw new RuntimeException('Unknown client_id');
}
return $claims;
}The PHP snippet caches the key set for an hour with APCu when it is installed. Without APCu, the static cache lasts one request under PHP-FPM, so every request fetches the keys again. A failed fetch is never cached: the snippet throws, and the next request tries again. Refetch the keys when you see an unknown kid, because NewInstance publishes the next key before it switches to it, and never cache them for longer than a day.
Map the customerlink
The sub claim is your own customer id, copied from the chat session your backend created. Use it to look up the customer in your system.
Other useful claims:
| Claim | Meaning |
|---|---|
scope | mcp:tools plus tool:<name> for the tool being called. Reject a call whose scope does not name the tool. |
jti | Unique per token. Log it so you can match a call to NewInstance's call log. |
Ignore identity-like tool arguments such as customerId, userId or accountOwner. The model fills arguments in, so they can be wrong or manipulated. Only sub says who the customer is, and the dashboard warns on any tool whose schema asks for one.
Discovery requestslink
When NewInstance connects to list your tools, it uses a discovery token. Its scope is mcp:discover and its sub is connection:<id> instead of a customer id. Answer initialize, tools/list and resources/list for it, and never return customer data for it.
Authorize every calllink
A valid token proves the call comes from NewInstance for that customer. It does not prove the customer may do what the tool does. Check in your own backend, on every call, that the customer in sub may perform this action on this resource, exactly as you would for a request from your own app. Verify the token again in your backend, or forward sub over a channel you already trust.
Tools that move money or change data need the strictest checks, and the customer should confirm them (see Steps that need the customer).
Create chat sessions with a server-only keylink
Chat sessions that may use tools must be created by your backend with a server-only API key. Create the key in the dashboard with the "Server only" switch on. It cannot be changed afterwards, because a key that was ever usable from an app may already have leaked, so create a new key instead.
Use a server-only key from your backend to create chat sessions. Chat apps cannot use it, and only sessions created with it can use your MCP server. Sessions created with any other key still chat, but without tools. See Session tokens (REST API) for the request and response.
The customer id you pass when creating the session becomes sub in every token for that session. NewInstance reads the key again on every tool call, so revoking the key ends tool access right away.
Steps that need the customerlink
When a tool needs the customer to confirm or provide something, your server asks through MCP elicitation inside the same tool call. The call stays open while the customer answers, for up to 10 minutes, then the step times out. NewInstance relays your steps exactly and never adds, skips or reorders them.
Customers see steps in chat SDK 0.5.0 or later. For a customer on an older SDK, NewInstance does not advertise the elicitation capability when it connects to your server. Check capabilities.elicitation in the initialize request (or catch the error from elicitInput), and fail the tool safely with a result that says the step is not available in this app. Never carry on as if the customer had answered.
Confirmationslink
Use form elicitation for non-sensitive questions, such as "Send N5,000 to Ada?". Send a JSON Schema with the fields you need. The customer answers or declines, and your tool call continues. The assistant learns only that the customer confirmed or declined, never the values.
Prefer confirming inside the tool call over a separate "confirm" tool in a later message, because the customer cannot skip or reorder the steps.
Form fieldslink
Forms are flat: each entry in properties is one field. The chat apps render this subset of JSON Schema:
| Schema | Shown as |
|---|---|
string | A text field |
string with enum, or with oneOf entries that have a const and a title | A single choice. A oneOf entry shows its title and sends its const. enumNames, when it has as many entries as enum, labels the enum values. |
number or integer | A number field. integer accepts whole numbers only. |
boolean | A toggle |
title is the field's label, description is the help text under it, and default fills it in. Fields listed in required must be filled before the customer can send. NewInstance checks minLength, maxLength, minimum and maximum on the answer and refuses one that breaks them, so the customer is asked to try again.
Anything else, such as an object or array property, is skipped. Never list a skipped field in required: the card then offers only Not now, so the customer can only decline.
iOS shows fields in property-name order, while Android and web keep the order of your schema. Name the properties so that alphabetical order also reads well, for example 1_amount before 2_note, and give every property a title, otherwise the key is shown as the label.
Secretslink
Never ask for a PIN, one-time code, password or card number in chat or in a form. Use URL elicitation instead. Send an https link to your own page, and the customer types the secret there in an in-app browser. The secret never touches NewInstance or the assistant. Links that are not https are declined.
When the customer opens your page, your tool receives accept. That means only that the page was opened, not that the customer finished it, so never act on accept alone. Keep the tool call waiting until your own backend confirms the page was completed, then send notifications/elicitation/complete with the elicitation id and finish the call. The chat then shows Done for the step. Send it within 10 minutes of asking, or the call times out.
Short referenceslink
Customers read tool results in chat and quote them back to the assistant. Return short, readable references such as TRF-4821 instead of long ids, and accept those references in your follow-up tools. Keep results small: return what the customer needs to see, not whole records.
Skillslink
Skills are optional but recommended. A skill is a short guide for the assistant on how your tools fit together, such as "check the balance before offering a transfer". Publish each one as an MCP resource whose URI starts with skill://, for example skill://transfers/SKILL.md, and list it in resources/list. NewInstance reads them when it discovers your server and gives them to the assistant as guidance.
Skill text, tool descriptions and tool results are treated as untrusted content. They help the assistant use your tools well, but they never override NewInstance's rules, and they cannot change what a customer is allowed to do. Your backend checks still decide that.
The dashboard recommends skills when a connection has none, and refreshes your tools and skills every few hours and whenever you ask.
Your own sign-in servicelink
If your backend should issue its own tokens, list your own authorization server in authorization_servers instead of the NewInstance issuer. NewInstance then exchanges its token for one of yours on every call. It prefers its own issuer when both are listed.
- Serve OAuth authorization server metadata (RFC 8414) that advertises the grant type
urn:ietf:params:oauth:grant-type:token-exchange(RFC 8693) and the client authentication methodprivate_key_jwt. - Register NewInstance as a client of your authorization server, with this connection's JWKS address as the client's keys address. Each token-exchange connection has its own, shown in the dashboard as the connection's JWKS address, in the form
https://service.newinstance.cloud/chat-mcp/connections/<connection id>/jwks.json. NewInstance signs this connection's client assertions with those keys. Do not register the platform JWKS for the client. - Enter the client ID your authorization server issued in the connection settings in the dashboard.
For each tool call NewInstance sends your token endpoint a token-exchange request. The subject_token is a NewInstance JWT with your authorization server as its audience, the subject_token_type is urn:ietf:params:oauth:token-type:jwt, and the resource is your MCP server. The subject token's sub is your customer id. Return an access token for that customer and server, and your MCP server verifies it the way you choose.
Before you issue anything, verify the subject token the way Verify NewInstance tokens above describes: ES256 against the platform keys at https://service.newinstance.cloud/chat-mcp/.well-known/jwks.json, with your authorization server's issuer as the audience. Require its client_id to equal this connection's id, which the dashboard shows as Client ID for this connection, so a token minted for another connection is refused.
NewInstance sends your token endpoint:
| Parameter | Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | An ES256 JWT (typ of at+jwt) whose aud is your authorization server's issuer exactly as published in its metadata |
scope | mcp:tools tool:<name> |
resource | Your MCP server |
client_assertion | A JWT with iss and sub set to the client ID, aud set to your token endpoint, valid for 60 seconds, signed ES256. Verify it with the same keys address you registered. |
Refuse a call by returning HTTP 400 (or 401 for a failed client authentication) with a JSON body holding an OAuth error code. invalid_client, unauthorized_client, unsupported_grant_type and invalid_target turn the connection off and alert the merchant, so use them only when the client or your setup is wrong. Any other code, such as invalid_grant or access_denied, refuses just that call. A 5xx response or a body that is not JSON counts as temporarily unavailable.