Перейти к документации
ДокументацияHTTP-прокси

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

Shell · GET через подключение 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 тоже.

Отправьте изменение

Shell · POST с intent
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 и такое тело:

JSON · 202, пока вызов ждёт
{
  "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 дойдёт до сервиса, а его выбор вернётся к вам.

JavaScript · распознавание речи ElevenLabs в реальном времени, пакет ws
import 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 впишет ключ до того, как сообщение дойдёт до сервиса, а вызов в разделе История вызовов покажет ваше сообщение таким, каким вы его отправили.

JavaScript · OpenAI Realtime, текстовый ответ
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()
})
JavaScript · распознавание речи Deepgram в реальном времени
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Путь выходит за ограничение пути подключения, например через ...
Коды 409IDEMPOTENCY_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 и повторите.
Коды 424EGRESS_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. Все поля необязательны, каждое заменяет свою часть запроса.

Shell · DELETE, описанный в POST
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.