HTTP-прокси
Отправляйте любой HTTP-запрос к подключённому сервису через Sallyport. Ключ подставляет шлюз, ваш скрипт его не видит.
На этой странице
Адрес
Отправьте запрос на https://mcp.sallyport.cloud/p/{binding_id}/{path} с токеном агента в Authorization: Bearer. Sallyport подставит ключ подключения, вызовет сервис и передаст его ответ потоком.
Сервис получит https://{host}{path_prefix}, за ним путь, который вы написали после ID, и строку запроса. У подключения из каталога префикс /, поэтому пишите полный путь из документации API поставщика.
Подключению, которое покрывает все хосты сервиса, например AWS на *.amazonaws.com, хост передают первым сегментом: /p/{binding_id}/sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15.
Что понадобится
- Токен агента. Откройте агента в разделе Агенты и нажмите «Новый токен» в блоке «Токены». Значение показывается один раз.
- ID HTTP-подключения. Откройте Подключения и выберите нужное: ID стоит в конце адреса страницы.
- Агент и подключение из одного рабочего пространства и одной среды. Sallyport выдаёт каждый новый ключ всем агентам пространства, так что обычно это уже выполнено.
Прочитайте свой аккаунт GitHub
export SP_AGENT_TOKEN='sp_live_…'
export SP_BINDING_ID='…'
curl --fail-with-body \
--header "Authorization: Bearer $SP_AGENT_TOKEN" \
"https://mcp.sallyport.cloud/p/$SP_BINDING_ID/user"Вернутся статус, заголовки и JSON самого GitHub, а X-Sallyport-Call-Id назовёт вызов. Ответ идёт потоком частями без Content-Length, и доходят все заголовки, Set-Cookie тоже.
Отправьте изменение
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $SP_AGENT_TOKEN" \
--header "Content-Type: application/json" \
--header "X-Sallyport-Intent: File the bug the user reported" \
--data '{"title":"Checkout fails on Safari"}' \
"https://mcp.sallyport.cloud/p/$SP_BINDING_ID/repos/acme/web/issues"| Заголовок | Что делает |
|---|---|
X-Sallyport-Intent | Одно предложение до 500 символов о том, зачем нужен вызов. Его читает человек на карточке подтверждения. Вызов, который ждёт человека без этого заголовка, получит 400 с reason: "missing_intent". |
X-Sallyport-Idempotency-Key | Ваш собственный ключ для изменения. Повтор с тем же ключом и тем же запросом 24 часа получает первый ответ и не выполняется снова. |
X-Sallyport-Call-Id | Продолжает вызов, ID которого у вас уже есть, что бы ни было в остальном запросе. Второй вызов не создаётся. |
Эти три заголовка Sallyport никуда не передаёт. Ещё он убирает Authorization, Cookie, Host, Content-Length, Expect, Forwarded, Via и все заголовки X-Forwarded-*.
Заголовок на месте ключа, например PRIVATE-TOKEN у GitLab, убирается. Его имя записывается в журнал, а вызов в истории вызовов показывает и значение с пометкой, что оно не отправлено. Подмена метода через _method и X-HTTP-Method-Override тоже убирается, поэтому выполняется тот метод, который вы отправили.
Повторы
Запросы GET, HEAD и OPTIONS выполняются всегда. Изменение без вашего ключа получает ключ, вычисленный из самого запроса, поэтому одинаковые изменения считаются одним вызовом.
Если повторить его, пока первый ждёт человека, повтор присоединится к той же карточке. Пока первый выполняется, повтор получит его ответ или 409 IN_FLIGHT. Ещё 2 минуты после завершения повтор получает первый ответ.
Возвращённый ответ несёт X-Sallyport-Deduplicated: true и X-Sallyport-Call-Id первого вызова. Если сервис отказал с кодом 4xx, ключ освобождается и исправленный повтор выполнится. Тот же ключ с другим запросом получит 409 IDEMPOTENCY_CONFLICT.
Когда нужно подтверждение человека
Вызов ждёт, если правило агента для этого подключения говорит «спросить», например после включения «Спрашивать каждый раз». Ответ приходит сразу: 202, Retry-After и такое тело:
{
"code": "APPROVAL_PENDING",
"instruction": "Show the user approve_url. After poll_after_ms repeat this same call with call_id — you get the result or \"still waiting\" again. This continues the same wait; it is not an idempotent cache. Do not create a new call.",
"call_id": "sp_c_4pQx…",
"approve_url": "https://my.sallyport.cloud/approve/sp_c_4pQx…",
"class": "write",
"summary": "POST api.github.com/repos/acme/web/issues",
"expires_at": "2026-09-29T19:05:00Z",
"poll_after_ms": 5000,
"expected_wait_ms": 42000,
"poll_url": "https://mcp.sallyport.cloud/p/calls/sp_c_4pQx…"
}Покажите человеку approve_url. Затем через Retry-After секунд запросите poll_url с тем же токеном. Подойдёт любой метод, и ничего не выполнится дважды.
| Ответ на опрос | Что значит |
|---|---|
202 с APPROVAL_PENDING | Никто ещё не ответил. Спросите снова через Retry-After. |
| Собственный ответ сервиса | Человек подтвердил. Вызов выполняется сейчас, и его ответ идёт потоком в этот опрос. |
202 с state | Вызов ещё у сервиса. Спросите снова через секунду. |
403 APPROVAL_DENIED | Человек отказал. В reason и decided_by написано почему и кто. Ничего не отправлено. |
410 APPROVAL_EXPIRED | Никто не ответил вовремя. Отправьте запрос заново без X-Sallyport-Call-Id, появится новая карточка. |
404 CALL_NOT_FOUND | Этот call_id принадлежит другому агенту или не существует. Сделайте новый вызов. |
200 с state и bytes | Вызов завершён, его ответ уже был отдан. В status статус сервиса, а body_ref есть, если ответ сохранён. |
error_code с state и bytes | Вызов завершился ошибкой, например UPSTREAM_TOO_LARGE. Статус ответа соответствует этому коду, а instruction говорит, что делать. |
Ответ длиннее 1 МиБ ещё и сохраняется, по умолчанию на 30 дней. Опрос завершённого вызова возвращает его body_ref, а у вызова в разделе История вызовов есть кнопка «Скачать весь ответ». Скачайте его через GET https://mcp.sallyport.cloud/v1/bodies/{body_ref} с тем же токеном агента; Range поддерживается.
WebSocket
Откройте wss://mcp.sallyport.cloud/p/{binding_id}/{path} с тем же токеном агента в Authorization: Bearer. Sallyport подставит ключ подключения туда, где его читает сервис (у большинства сервисов это рукопожатие), откроет wss://{host}{path} у сервиса и передаст каждый кадр в обе стороны таким, каким его отправили: текст, бинарные данные, ping, pong и код закрытия. Подпротокол из Sec-WebSocket-Protocol дойдёт до сервиса, а его выбор вернётся к вам.
wsimport WebSocket from 'ws'
const socket = new WebSocket(
`wss://mcp.sallyport.cloud/p/${process.env.SP_BINDING_ID}/v1/speech-to-text/realtime` +
'?model_id=scribe_v2_realtime&audio_format=pcm_16000&commit_strategy=vad',
{ headers: { Authorization: `Bearer ${process.env.SP_AGENT_TOKEN}` } },
)
// 16 kHz 16-bit mono PCM, every 20–100 ms
function sendAudio(pcm) {
socket.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: pcm.toString('base64') }))
}
socket.on('message', (data) => {
const event = JSON.parse(data.toString())
if (event.message_type === 'committed_transcript') console.log(event.text)
})
// 202: the session waits for a person. 4xx: the refusal, as JSON.
socket.on('unexpected-response', (_request, response) => console.error(response.statusCode))Сессия считается одним вызовом, и решает о нём правило агента для подключения, как о любом другом. Распознавание речи ElevenLabs в реальном времени считается платной записью и выполняется сразу, если не включено «Спрашивать каждый раз». Вызов в Активности показывает, когда сессия открылась и закрылась, кто её закрыл и с каким кодом, что отправила каждая сторона и каждое сообщение в обе стороны таким, каким его отправили. Сообщения хранятся 30 дней, как длинный ответ.
| Лимит | Значение | Что дальше |
|---|---|---|
| Длина сессии | 60 минут | Обе стороны получают закрытие 1001 с причиной. |
| Одно сообщение в любую сторону | 1 МиБ | Обе стороны получают закрытие 1009. Сообщение дальше не передаётся. |
| Тишина в обе стороны | 10 минут без кадра | Обе стороны получают закрытие 1001 с причиной. |
Если сессия должна ждать человека, на рукопожатие приходит 202 и тело APPROVAL_PENDING, как выше, а не 101. Когда человек одобрит, откройте WebSocket снова с X-Sallyport-Call-Id: {call_id}, и одобренная сессия начнётся. Опрос poll_url в этот момент отвечает 426 WEBSOCKET_REQUIRED: сессия идёт только по WebSocket. Если сервис отказал в рукопожатии, из-за неверного ключа или параметра, вы получите его собственный статус и тело.
Сервисы, которые работают по WebSocket
Каждый из них открывает свой WebSocket API через /p/, а ключ Sallyport кладёт туда, где его читает сервис. Адрес и сообщения вы пишете так, как их описывает документация сервиса. list_access с service скажет то же самое про каждое подключение.
| Сервисы | Куда Sallyport кладёт ключ |
|---|---|
| OpenAI Realtime, Azure OpenAI Realtime, Gemini Live, xAI, Mistral, Deepgram, AssemblyAI, Speechmatics, Cartesia, MiniMax, Together AI, fal, Murf, Telnyx, ElevenLabs, Retell, Gladia | В рукопожатие, как в каждый вызов |
| Rev.ai, Hume | В строку запроса: access_token, api_key |
| Рыночные данные Alpaca, CoinGecko, CoinMarketCap, Alchemy | В рукопожатие |
| Finnhub, Twelve Data | В строку запроса: token, apikey |
| Massive, Financial Modeling Prep, поток сделок Alpaca | В первое сообщение, которое вы отправите |
| Binance, Bybit, OKX, Kraken, Coinbase, Coinbase Exchange | Никуда: публичным потокам ключ не нужен, а закрытый поток Kraken принимает токен из ответа на вызов GetWebSocketsToken |
| Шлюз Discord | В первое сообщение, которое вы отправите: d.token в IDENTIFY |
| Slack Socket Mode, Twitch EventSub, RingCentral, Vapi, консоль Hetzner | Никуда: откройте адрес из ответа сервиса вместе с его билетом |
| Railway, Render, Koyeb, Daytona, Mattermost, ntfy, Grafana Cloud | В рукопожатие |
| Browserless, Steel, Browserbase | В строку запроса: token, apiKey |
Некоторые сервисы держат WebSocket на отдельном хосте: gateway.discord.gg, agent.deepgram.com, ws.finnhub.io, wss-….slack.com из билета Socket Mode. Такой хост открывается через подключение этого сервиса, первым сегментом пути: wss://mcp.sallyport.cloud/p/{binding_id}/gateway.discord.gg/?v=10&encoding=json. Попасть на него можно только по WebSocket.
Если ключ идёт в первое сообщение, отправьте сообщение из документации сервиса с "{{credential}}" на месте ключа или без этого поля. Sallyport впишет ключ до того, как сообщение дойдёт до сервиса, а вызов в разделе История вызовов покажет ваше сообщение таким, каким вы его отправили.
import WebSocket from 'ws'
const socket = new WebSocket(
`wss://mcp.sallyport.cloud/p/${process.env.SP_BINDING_ID}/v1/realtime?model=gpt-realtime`,
{ headers: { Authorization: `Bearer ${process.env.SP_AGENT_TOKEN}` } },
)
socket.on('open', () => {
socket.send(JSON.stringify({ type: 'session.update', session: { type: 'realtime', output_modalities: ['text'] } }))
socket.send(JSON.stringify({
type: 'conversation.item.create',
item: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Say hello in one sentence.' }] },
}))
socket.send(JSON.stringify({ type: 'response.create' }))
})
socket.on('message', (data) => {
const event = JSON.parse(data.toString())
if (event.type === 'response.output_text.delta') process.stdout.write(event.delta)
if (event.type === 'response.done') socket.close()
})import WebSocket from 'ws'
const socket = new WebSocket(
`wss://mcp.sallyport.cloud/p/${process.env.SP_BINDING_ID}/v1/listen` +
'?model=nova-3&encoding=linear16&sample_rate=16000&interim_results=true',
{ headers: { Authorization: `Bearer ${process.env.SP_AGENT_TOKEN}` } },
)
// 16 kHz 16-bit mono PCM, as it arrives
function sendAudio(pcm) {
socket.send(pcm)
}
socket.on('message', (data) => {
const result = JSON.parse(data.toString())
if (result.type === 'Results' && result.is_final) console.log(result.channel.alternatives[0].transcript)
})
// When the audio ends, Deepgram sends the last results and closes.
function finish() {
socket.send(JSON.stringify({ type: 'CloseStream' }))
}Отказы
Отказ приходит в виде JSON: {"error": {"code": …, "instruction": …}}, рядом лежат поля, нужные этому коду. В instruction одно предложение на английском о том, что делать дальше.
Четыре кода считаются результатом, а не ошибкой, и приходят простым объектом без обёртки error: APPROVAL_PENDING, ACCESS_REQUEST_CREATED, ALREADY_EXECUTED и UPSTREAM_ERROR. На /p/ тело с UPSTREAM_ERROR приходит со статусом 424, когда сервис так и не ответил. Если сервис ответил ошибкой, вы получите его собственный ответ.
| Статус и код | Что случилось |
|---|---|
401 AUTH_FAILED | Токена нет, он неверный, отозван или выдан только для /mcp. WWW-Authenticate подсказывает OAuth-клиенту, где взять нужный. |
403 SOURCE_IP_NOT_ALLOWED | Запрос пришёл с адреса, которого нет в списке разрешённых для агента. |
423 SUSPENDED | Агент, его рабочее пространство или вся организация приостановлены. |
404 ACCESS_REQUEST_CREATED | Агент не может пользоваться этим подключением: нет доступа, подключение приостановлено или ID неизвестен. Покажите человеку approve_url и повторите вызов. |
403 POLICY_DENY | Правило агента для этого подключения запрещает вызовы этого класса. |
404 CALL_NOT_FOUND | Вызов из X-Sallyport-Call-Id или из опроса принадлежит другому агенту или не существует. Сделайте новый вызов. |
400 BAD_REQUEST | В reason написано, что исправить: missing_intent, intent_too_long, body_too_large с max_bytes, missing_host, query_in_path, mcp_binding, database_binding, email_binding, email_operation, sftp_operation, websocket_handshake и другое. |
400 PATH_OUT_OF_PREFIX | Путь выходит за ограничение пути подключения, например через ... |
Коды 409 | IDEMPOTENCY_CONFLICT, IN_FLIGHT, CALL_CONFLICT или APPROVAL_SUPERSEDED. Что делать, написано в instruction. |
426 UPGRADE_NOT_SUPPORTED | Апгрейд до любого протокола, кроме WebSocket. |
426 WEBSOCKET_REQUIRED | Опрос одобренной сессии WebSocket. Откройте WebSocket снова с X-Sallyport-Call-Id. |
429 RATE_LIMITED | Сработало ограничение частоты у агента или подключения. Подождите Retry-After и повторите. |
Коды 424 | EGRESS_DENIED, SERVICE_NOT_CONNECTED, UPSTREAM_TOO_LARGE, OUTCOME_UNKNOWN или UPSTREAM_ERROR. Прочитайте instruction перед повтором: OUTCOME_UNKNOWN значит, что сервис мог выполнить действие. |
503 UNAVAILABLE, 500 INTERNAL | Сбой на стороне Sallyport. После 503 повторите через Retry-After. |
Опишите запрос в теле
Клиент, который не может задать метод или путь, может отправить POST с описанием запроса и Content-Type: application/vnd.sallyport.request+json. Все поля необязательны, каждое заменяет свою часть запроса.
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $SP_AGENT_TOKEN" \
--header "Content-Type: application/vnd.sallyport.request+json" \
--data '{"method":"DELETE","path":"/repos/acme/web/labels/stale","intent":"Remove the unused label"}' \
"https://mcp.sallyport.cloud/p/$SP_BINDING_ID/"Поля: method, path, query, headers, body, intent и idempotency_key. В path нельзя ставить ? и #, параметры передают в query. body в виде JSON уходит как JSON, строка уходит как есть.
Ответ может занимать до 512 МиБ. Лимит конкретного подключения виден на его странице в блоке «Лимиты», в строке «Самый большой ответ сервиса». Ответ идёт потоком по мере получения, поэтому слишком большой ответ обрывается. Статус и заголовки к этому моменту уже отправлены, а тело приходит не целиком. Опрос вызова, названного в X-Sallyport-Call-Id, после этого покажет UPSTREAM_TOO_LARGE.