Proxy HTTP
Envie qualquer requisição HTTP a um serviço conectado pelo Sallyport. O gateway coloca a chave, então seu script nunca a tem.
Nesta página
O endereço
Envie a requisição para https://mcp.sallyport.cloud/p/{binding_id}/{path} com um token de agente em Authorization: Bearer. O Sallyport coloca a chave da conexão, chama o serviço e devolve a resposta em streaming.
O serviço recebe https://{host}{path_prefix}, depois o caminho que você escreveu após o ID e depois sua query string. Uma conexão do catálogo tem o prefixo /, então escreva o caminho completo da documentação da API do fornecedor.
Uma conexão que cobre todos os hosts de um serviço, como a AWS em *.amazonaws.com, recebe o host como primeiro segmento: /p/{binding_id}/sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15.
O que você precisa
- Um token de agente. Abra o agente em Agentes e clique em Novo token na seção Tokens. O valor aparece uma única vez.
- O ID de uma conexão HTTP. Abra Conexões e clique na conexão: o ID é a última parte do endereço da página.
- Um agente e uma conexão do mesmo espaço de trabalho e ambiente. O Sallyport dá cada chave nova a todos os agentes do espaço de trabalho, então isso já vale, a menos que alguém tenha removido.
Leia sua conta do 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"Voltam o status, os cabeçalhos e o JSON do próprio GitHub, com X-Sallyport-Call-Id, que nomeia a chamada. A resposta chega em partes sem Content-Length, e chegam todos os cabeçalhos, inclusive o Set-Cookie.
Envie uma escrita
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"| Cabeçalho | O que faz |
|---|---|
X-Sallyport-Intent | Uma frase de até 500 caracteres sobre por que a chamada é feita. Uma pessoa a lê no cartão de aprovação. Uma chamada que espera uma pessoa sem ela recebe 400 com reason: "missing_intent". |
X-Sallyport-Idempotency-Key | Sua própria chave para uma escrita. Uma repetição com a mesma chave e a mesma requisição recebe a primeira resposta por 24 horas em vez de rodar de novo. |
X-Sallyport-Call-Id | Continua uma chamada cujo ID você já tem, seja qual for o resto da requisição. Nenhuma segunda chamada é criada. |
O Sallyport nunca repassa esses três cabeçalhos. Ele também remove Authorization, Cookie, Host, Content-Length, Expect, Forwarded, Via e todo cabeçalho X-Forwarded-*.
Um cabeçalho no lugar da chave, como o PRIVATE-TOKEN do GitLab, é removido. O nome dele vai para o registro, e a chamada em Atividade mostra o valor, marcado como não enviado. Trocas de método como _method e X-HTTP-Method-Override são removidas, então roda o método que você enviou.
Repetições
Uma requisição GET, HEAD ou OPTIONS sempre roda. Uma escrita sem sua própria chave recebe uma calculada a partir da requisição, então duas escritas idênticas são uma única chamada.
Enviada de novo enquanto a primeira espera uma pessoa, ela entra no mesmo cartão. Enquanto a primeira roda, recebe essa resposta ou 409 IN_FLIGHT. Por 2 minutos após o fim, recebe a primeira resposta.
Uma resposta devolvida traz X-Sallyport-Deduplicated: true e o X-Sallyport-Call-Id da primeira chamada. Se o serviço recusou a escrita com um 4xx, a chave é liberada e a repetição corrigida roda. A mesma chave com outra requisição recebe 409 IDEMPOTENCY_CONFLICT.
Quando uma pessoa precisa aprovar
Uma chamada espera quando a regra do agente para aquela conexão diz perguntar, por exemplo depois de ligar Perguntar sempre. A resposta chega na hora, com 202, Retry-After e este corpo:
{
"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…"
}Mostre approve_url à pessoa. Depois consulte poll_url com o mesmo token após Retry-After segundos. Qualquer método serve, e nada roda duas vezes.
| Resposta da consulta | Significado |
|---|---|
202 com APPROVAL_PENDING | Ninguém respondeu ainda. Pergunte de novo após Retry-After. |
| A resposta do próprio serviço | A pessoa aprovou. A chamada roda agora e a resposta chega nesta consulta. |
202 com state | A chamada ainda está com o serviço. Pergunte de novo em um segundo. |
403 APPROVAL_DENIED | A pessoa recusou. reason e decided_by dizem por que e quem. Nada foi enviado. |
410 APPROVAL_EXPIRED | Ninguém respondeu a tempo. Envie a requisição de novo sem X-Sallyport-Call-Id para um cartão novo. |
404 CALL_NOT_FOUND | O call_id pertence a outro agente ou não existe. Faça uma chamada nova. |
200 com state e bytes | A chamada terminou e a resposta já saiu. status é o status do serviço, e body_ref aparece quando a resposta foi guardada. |
error_code com state e bytes | A chamada terminou com uma falha, como UPSTREAM_TOO_LARGE. A linha de status é a desse código, e instruction diz o que fazer. |
Uma resposta com mais de 1 MiB também é guardada, por 30 dias por padrão. Uma consulta da chamada terminada traz o body_ref dela, e a chamada em Atividade oferece Baixar a resposta completa. Baixe com GET https://mcp.sallyport.cloud/v1/bodies/{body_ref} e o mesmo token de agente; Range funciona.
WebSocket
Abra wss://mcp.sallyport.cloud/p/{binding_id}/{path} com o mesmo token de agente em Authorization: Bearer. O Sallyport põe a chave da conexão onde o serviço a lê, no handshake para a maioria dos serviços, abre wss://{host}{path} no serviço e passa cada frame nos dois sentidos como foi enviado: texto, binário, ping, pong e o código de fechamento. O subprotocolo que você pede em Sec-WebSocket-Protocol chega ao serviço, e a escolha dele volta.
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))A sessão é uma chamada, decidida como qualquer outra pela regra do agente para a conexão. A transcrição em tempo real da ElevenLabs é uma escrita paga e roda direto, a não ser que Perguntar sempre esteja ligado. A chamada em Atividade mostra quando a sessão abriu e fechou, quem a fechou e com qual código, o que cada lado enviou e cada mensagem nos dois sentidos como foi enviada. As mensagens ficam guardadas 30 dias, como uma resposta longa.
| Limite | Valor | Além do limite |
|---|---|---|
| Duração da sessão | 60 minutos | Os dois lados recebem o fechamento 1001 com o motivo. |
| Uma mensagem, em qualquer sentido | 1 MiB | Os dois lados recebem o fechamento 1009. A mensagem não é repassada. |
| Silêncio nos dois sentidos | 10 minutos sem frame | Os dois lados recebem o fechamento 1001 com o motivo. |
Quando a sessão precisa esperar uma pessoa, o handshake recebe 202 e o corpo APPROVAL_PENDING acima em vez de 101. Quando a pessoa aprovar, abra o WebSocket de novo com X-Sallyport-Call-Id: {call_id} e a sessão aprovada começa. Uma consulta a poll_url responde então 426 WEBSOCKET_REQUIRED, porque uma sessão só roda sobre um WebSocket. Um serviço que recusa o handshake, por uma chave ou um parâmetro errado, envia o próprio status e corpo.
Serviços que funcionam por WebSocket
Cada um abre sua API WebSocket pelo /p/, e o Sallyport põe a chave onde o fornecedor a lê. Você escreve o endereço e as mensagens como o fornecedor documenta. list_access com service diz o mesmo para cada conexão.
| Serviços | Onde o Sallyport põe a chave |
|---|---|
| OpenAI Realtime, Azure OpenAI Realtime, Gemini Live, xAI, Mistral, Deepgram, AssemblyAI, Speechmatics, Cartesia, MiniMax, Together AI, fal, Murf, Telnyx, ElevenLabs, Retell, Gladia | No handshake, como em toda chamada |
| Rev.ai, Hume | Na query: access_token, api_key |
| Dados de mercado da Alpaca, CoinGecko, CoinMarketCap, Alchemy | No handshake |
| Finnhub, Twelve Data | Na query: token, apikey |
| Massive, Financial Modeling Prep, atualizações de ordens da Alpaca | Na primeira mensagem que você envia |
| Binance, Bybit, OKX, Kraken, Coinbase, Coinbase Exchange | Em lugar nenhum: os fluxos públicos não precisam dela, e o fluxo privado da Kraken recebe o token que a chamada GetWebSocketsToken responde |
| Gateway do Discord | Na primeira mensagem que você envia: o d.token do IDENTIFY |
| Slack Socket Mode, Twitch EventSub, RingCentral, Vapi, console da Hetzner | Em lugar nenhum: abra a URL com que o serviço respondeu, com o ticket |
| Railway, Render, Koyeb, Daytona, Mattermost, ntfy, Grafana Cloud | No handshake |
| Browserless, Steel, Browserbase | Na query: token, apiKey |
Alguns serviços rodam seu WebSocket num host próprio: gateway.discord.gg, agent.deepgram.com, ws.finnhub.io, o wss-….slack.com de um ticket do Socket Mode. Esse host abre pela conexão desse serviço, como primeiro segmento do caminho: wss://mcp.sallyport.cloud/p/{binding_id}/gateway.discord.gg/?v=10&encoding=json. Só um WebSocket chega a ele.
Quando a chave vai na primeira mensagem, envie a mensagem que o fornecedor documenta com "{{credential}}" no lugar da chave, ou sem esse campo. O Sallyport escreve a chave antes que a mensagem chegue ao serviço, e a chamada em Atividade mostra sua mensagem como você a enviou.
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' }))
}Recusas
Uma recusa é JSON: {"error": {"code": …, "instruction": …}}, com os campos que o código precisa ao lado. instruction é uma frase em inglês sobre o que fazer em seguida.
Quatro códigos são resultados, não erros, e chegam como um objeto simples sem o invólucro error: APPROVAL_PENDING, ACCESS_REQUEST_CREATED, ALREADY_EXECUTED e UPSTREAM_ERROR. Em /p/ um corpo UPSTREAM_ERROR é a resposta 424 quando o serviço nunca respondeu. Um serviço que responde com erro envia a você a própria resposta.
| Status e código | O que aconteceu |
|---|---|
401 AUTH_FAILED | O token está ausente, errado, revogado ou vale só para /mcp. WWW-Authenticate diz a um cliente OAuth onde obter o certo. |
403 SOURCE_IP_NOT_ALLOWED | A requisição veio de um endereço fora da lista permitida do agente. |
423 SUSPENDED | O agente, o espaço de trabalho dele ou a organização estão pausados. |
404 ACCESS_REQUEST_CREATED | O agente não pode usar esta conexão: sem acesso, conexão pausada ou ID desconhecido. Mostre approve_url a uma pessoa e repita a chamada. |
403 POLICY_DENY | A regra do agente para esta conexão recusa essa classe de chamada. |
404 CALL_NOT_FOUND | O X-Sallyport-Call-Id ou a chamada consultada pertence a outro agente ou não existe. Faça uma chamada nova. |
400 BAD_REQUEST | reason diz o que corrigir: missing_intent, intent_too_long, body_too_large com max_bytes, missing_host, query_in_path, mcp_binding, database_binding, email_binding, email_operation, sftp_operation, websocket_handshake e outros. |
400 PATH_OUT_OF_PREFIX | O caminho sai do limite de caminho da conexão, por exemplo com ... |
Códigos 409 | IDEMPOTENCY_CONFLICT, IN_FLIGHT, CALL_CONFLICT ou APPROVAL_SUPERSEDED. A instruction diz o que fazer. |
426 UPGRADE_NOT_SUPPORTED | Um upgrade para qualquer protocolo que não seja WebSocket. |
426 WEBSOCKET_REQUIRED | Uma consulta de uma sessão WebSocket aprovada. Abra o WebSocket de novo com X-Sallyport-Call-Id. |
429 RATE_LIMITED | Um limite de frequência do agente ou da conexão. Espere Retry-After e repita. |
Códigos 424 | EGRESS_DENIED, SERVICE_NOT_CONNECTED, UPSTREAM_TOO_LARGE, OUTCOME_UNKNOWN ou UPSTREAM_ERROR. Leia a instruction antes de repetir: OUTCOME_UNKNOWN significa que o serviço pode ter agido. |
503 UNAVAILABLE, 500 INTERNAL | Uma falha do lado do Sallyport. Repita um 503 após Retry-After. |
Descreva a requisição no corpo
Um cliente que não consegue definir o método ou o caminho pode fazer POST de uma descrição com Content-Type: application/vnd.sallyport.request+json. Todos os campos são opcionais e cada um substitui aquela parte da requisição.
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/"Os campos são method, path, query, headers, body, intent e idempotency_key. path não aceita ? nem #: os parâmetros vão em query. Um body JSON é enviado como JSON, e uma string como texto puro.
Uma resposta pode ter até 512 MiB. A página da conexão mostra o limite dela em Limites, como Maior resposta do serviço. A resposta é transmitida conforme chega, então uma que passa do limite é cortada. O status e os cabeçalhos já foram enviados, e o corpo fica incompleto. Uma consulta da chamada indicada em X-Sallyport-Call-Id mostra então UPSTREAM_TOO_LARGE.