Proxy HTTP
Envía cualquier solicitud HTTP a un servicio conectado a través de Sallyport. La pasarela pone la clave, así que tu script nunca la tiene.
En esta página
La dirección
Envía la solicitud a https://mcp.sallyport.cloud/p/{binding_id}/{path} con un token de agente en Authorization: Bearer. Sallyport pone la clave de la conexión, llama al servicio y te devuelve su respuesta en streaming.
El servicio recibe https://{host}{path_prefix}, luego la ruta que escribiste después del ID y luego tu query string. Una conexión del catálogo tiene el prefijo /, así que escribe la ruta completa de la documentación de la API del proveedor.
Una conexión que cubre todos los hosts de un servicio, como AWS en *.amazonaws.com, recibe el host como primer segmento: /p/{binding_id}/sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15.
Qué necesitas
- Un token de agente. Abre el agente en Agentes y pulsa Nuevo token en la sección Tokens. El valor se muestra una sola vez.
- El ID de una conexión HTTP. Abre Conexiones y haz clic en la conexión: el ID es la última parte de la dirección de la página.
- Un agente y una conexión del mismo espacio de trabajo y entorno. Sallyport da cada clave nueva a todos los agentes de su espacio, así que esto ya se cumple salvo que alguien lo haya quitado.
Lee tu cuenta de 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"Vuelven el estado, las cabeceras y el JSON del propio GitHub, con X-Sallyport-Call-Id, que nombra la llamada. La respuesta llega por partes sin Content-Length, y llegan todas las cabeceras, también Set-Cookie.
Envía una escritura
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"| Cabecera | Qué hace |
|---|---|
X-Sallyport-Intent | Una frase de hasta 500 caracteres sobre por qué se hace la llamada. Una persona la lee en la tarjeta de aprobación. Una llamada que espera a una persona sin ella recibe 400 con reason: "missing_intent". |
X-Sallyport-Idempotency-Key | Tu propia clave para una escritura. Una repetición con la misma clave y la misma solicitud recibe la primera respuesta durante 24 horas en lugar de ejecutarse otra vez. |
X-Sallyport-Call-Id | Continúa una llamada cuyo ID ya tienes, diga lo que diga el resto de la solicitud. No se crea una segunda llamada. |
Sallyport nunca reenvía estas tres cabeceras. También quita Authorization, Cookie, Host, Content-Length, Expect, Forwarded, Via y toda cabecera X-Forwarded-*.
Una cabecera en el lugar de la clave, como PRIVATE-TOKEN de GitLab, se quita. Su nombre queda en el registro, y la llamada en Actividad muestra su valor, marcado como no enviado. Los cambios de método como _method y X-HTTP-Method-Override se eliminan, así que se ejecuta el método que envías.
Repeticiones
Una solicitud GET, HEAD u OPTIONS siempre se ejecuta. Una escritura sin tu propia clave recibe una calculada a partir de la solicitud, así que dos escrituras idénticas son una sola llamada.
Si la repites mientras la primera espera a una persona, se une a la misma tarjeta. Mientras la primera se ejecuta, recibe esa respuesta o 409 IN_FLIGHT. Durante 2 minutos tras el final, recibe la primera respuesta.
Una respuesta devuelta lleva X-Sallyport-Deduplicated: true y el X-Sallyport-Call-Id de la primera llamada. Si el servicio rechazó la escritura con un 4xx, la clave se libera y la repetición corregida se ejecuta. La misma clave con otra solicitud recibe 409 IDEMPOTENCY_CONFLICT.
Cuando una persona tiene que aprobar
Una llamada espera cuando la regla del agente para esa conexión dice preguntar, por ejemplo tras activar Preguntar en cada llamada. La respuesta llega enseguida, con 202, Retry-After y este cuerpo:
{
"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…"
}Muestra approve_url a la persona. Luego consulta poll_url con el mismo token tras Retry-After segundos. Sirve cualquier método y nada se ejecuta dos veces.
| Respuesta a la consulta | Significado |
|---|---|
202 con APPROVAL_PENDING | Nadie ha respondido todavía. Vuelve a preguntar tras Retry-After. |
| La respuesta del propio servicio | La persona aprobó. La llamada se ejecuta ahora y su respuesta llega a esta consulta. |
202 con state | La llamada sigue en el servicio. Vuelve a preguntar en un segundo. |
403 APPROVAL_DENIED | La persona rechazó. reason y decided_by dicen por qué y quién. No se envió nada. |
410 APPROVAL_EXPIRED | Nadie respondió a tiempo. Envía la solicitud de nuevo sin X-Sallyport-Call-Id para una tarjeta nueva. |
404 CALL_NOT_FOUND | El call_id pertenece a otro agente o no existe. Haz una llamada nueva. |
200 con state y bytes | La llamada terminó y su respuesta ya salió. status es el estado del servicio, y body_ref aparece cuando la respuesta se guardó. |
error_code con state y bytes | La llamada terminó con un fallo, como UPSTREAM_TOO_LARGE. La línea de estado es la de ese código, e instruction dice qué hacer. |
Una respuesta de más de 1 MiB también se guarda, 30 días por defecto. Una consulta de la llamada terminada trae su body_ref, y la llamada en Actividad ofrece Descargar la respuesta completa. Descárgala con GET https://mcp.sallyport.cloud/v1/bodies/{body_ref} y el mismo token de agente; Range funciona.
WebSocket
Abre wss://mcp.sallyport.cloud/p/{binding_id}/{path} con el mismo token de agente en Authorization: Bearer. Sallyport pone la clave de la conexión donde el servicio la lee, en el handshake para la mayoría de los servicios, abre wss://{host}{path} en el servicio y pasa cada frame en ambos sentidos tal como se envió: texto, binario, ping, pong y el código de cierre. El subprotocolo que pides en Sec-WebSocket-Protocol llega al servicio, y su elección vuelve.
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))La sesión es una llamada, y la decide la regla del agente para la conexión, como cualquier otra. La transcripción en tiempo real de ElevenLabs es una escritura de pago y pasa directa salvo que Preguntar en cada llamada esté activado. La llamada en Actividad muestra cuándo se abrió y se cerró la sesión, quién la cerró y con qué código, qué envió cada lado y cada mensaje en ambos sentidos tal como se envió. Los mensajes se guardan 30 días, como una respuesta larga.
| Límite | Valor | Al pasarlo |
|---|---|---|
| Duración de la sesión | 60 minutos | Ambos lados reciben el cierre 1001 con el motivo. |
| Un mensaje, en cualquier sentido | 1 MiB | Ambos lados reciben el cierre 1009. El mensaje no se pasa. |
| Silencio en ambos sentidos | 10 minutos sin un frame | Ambos lados reciben el cierre 1001 con el motivo. |
Cuando la sesión tiene que esperar a una persona, el handshake recibe 202 y el cuerpo APPROVAL_PENDING de arriba en lugar de 101. Cuando la persona aprueba, abre de nuevo el WebSocket con X-Sallyport-Call-Id: {call_id} y empieza la sesión aprobada. Una consulta a poll_url responde entonces 426 WEBSOCKET_REQUIRED, porque una sesión solo corre sobre un WebSocket. Un servicio que rechaza el handshake, por una clave o un parámetro incorrectos, te envía su propio estado y cuerpo.
Servicios que funcionan por WebSocket
Cada uno abre su API WebSocket a través de /p/, y Sallyport pone la clave donde el proveedor la lee. Escribes la dirección y los mensajes tal como los documenta el proveedor. list_access con service dice lo mismo para cada conexión.
| Servicios | Dónde pone Sallyport la clave |
|---|---|
| OpenAI Realtime, Azure OpenAI Realtime, Gemini Live, xAI, Mistral, Deepgram, AssemblyAI, Speechmatics, Cartesia, MiniMax, Together AI, fal, Murf, Telnyx, ElevenLabs, Retell, Gladia | En el handshake, como en cada llamada |
| Rev.ai, Hume | En la query: access_token, api_key |
| Datos de mercado de Alpaca, CoinGecko, CoinMarketCap, Alchemy | En el handshake |
| Finnhub, Twelve Data | En la query: token, apikey |
| Massive, Financial Modeling Prep, actualizaciones de órdenes de Alpaca | En el primer mensaje que envías |
| Binance, Bybit, OKX, Kraken, Coinbase, Coinbase Exchange | En ninguna parte: los flujos públicos no la necesitan, y el flujo privado de Kraken toma el token que responde su llamada GetWebSocketsToken |
| Gateway de Discord | En el primer mensaje que envías: el d.token de IDENTIFY |
| Slack Socket Mode, Twitch EventSub, RingCentral, Vapi, consola de Hetzner | En ninguna parte: abre la URL con la que respondió el servicio, con su ticket |
| Railway, Render, Koyeb, Daytona, Mattermost, ntfy, Grafana Cloud | En el handshake |
| Browserless, Steel, Browserbase | En la query: token, apiKey |
Algunos servicios tienen su WebSocket en un host propio: gateway.discord.gg, agent.deepgram.com, ws.finnhub.io, el wss-….slack.com de un ticket de Socket Mode. Ese host se abre por la conexión de ese servicio, como primer segmento de la ruta: wss://mcp.sallyport.cloud/p/{binding_id}/gateway.discord.gg/?v=10&encoding=json. Solo un WebSocket llega a él.
Cuando la clave va en el primer mensaje, envía el mensaje que documenta el proveedor con "{{credential}}" en lugar de la clave, o sin ese campo. Sallyport escribe la clave antes de que el mensaje llegue al servicio, y la llamada en Actividad muestra tu mensaje tal como lo enviaste.
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' }))
}Rechazos
Un rechazo es JSON: {"error": {"code": …, "instruction": …}}, con los campos que su código necesita al lado. instruction es una frase en inglés sobre qué hacer después.
Cuatro códigos son resultados, no errores, y llegan como un objeto simple sin el envoltorio error: APPROVAL_PENDING, ACCESS_REQUEST_CREATED, ALREADY_EXECUTED y UPSTREAM_ERROR. En /p/ un cuerpo UPSTREAM_ERROR es la respuesta 424 cuando el servicio nunca respondió. Un servicio que responde con un error te envía su propia respuesta.
| Estado y código | Qué pasó |
|---|---|
401 AUTH_FAILED | El token falta, es incorrecto, está revocado o solo sirve para /mcp. WWW-Authenticate indica a un cliente OAuth dónde conseguir el correcto. |
403 SOURCE_IP_NOT_ALLOWED | La solicitud vino de una dirección fuera de la lista permitida del agente. |
423 SUSPENDED | El agente, su espacio de trabajo o la organización están en pausa. |
404 ACCESS_REQUEST_CREATED | El agente no puede usar esta conexión: sin acceso, conexión en pausa o ID desconocido. Muestra approve_url a una persona y repite la llamada. |
403 POLICY_DENY | La regla del agente para esta conexión rechaza esta clase de llamada. |
404 CALL_NOT_FOUND | El X-Sallyport-Call-Id o la llamada consultada pertenece a otro agente o no existe. Haz una llamada nueva. |
400 BAD_REQUEST | reason dice qué corregir: missing_intent, intent_too_long, body_too_large con max_bytes, missing_host, query_in_path, mcp_binding, database_binding, email_binding, email_operation, sftp_operation, websocket_handshake y otros. |
400 PATH_OUT_OF_PREFIX | La ruta sale del límite de ruta de la conexión, por ejemplo con ... |
Códigos 409 | IDEMPOTENCY_CONFLICT, IN_FLIGHT, CALL_CONFLICT o APPROVAL_SUPERSEDED. La instruction dice qué hacer. |
426 UPGRADE_NOT_SUPPORTED | Un upgrade a cualquier protocolo que no sea WebSocket. |
426 WEBSOCKET_REQUIRED | Una consulta de una sesión WebSocket aprobada. Abre de nuevo el WebSocket con X-Sallyport-Call-Id. |
429 RATE_LIMITED | Un límite de frecuencia del agente o de la conexión. Espera Retry-After y repite. |
Códigos 424 | EGRESS_DENIED, SERVICE_NOT_CONNECTED, UPSTREAM_TOO_LARGE, OUTCOME_UNKNOWN o UPSTREAM_ERROR. Lee la instruction antes de repetir: OUTCOME_UNKNOWN significa que el servicio pudo actuar. |
503 UNAVAILABLE, 500 INTERNAL | Un fallo del lado de Sallyport. Repite un 503 tras Retry-After. |
Describe la solicitud en el cuerpo
Un cliente que no puede fijar el método o la ruta puede hacer POST con una descripción y Content-Type: application/vnd.sallyport.request+json. Todos los campos son opcionales y cada uno sustituye esa parte de la solicitud.
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/"Los campos son method, path, query, headers, body, intent e idempotency_key. path no admite ? ni #: los parámetros van en query. Un body JSON se envía como JSON, y una cadena como texto tal cual.
Una respuesta admite hasta 512 MiB. La página de la conexión muestra su propio límite en Límites, como Respuesta más grande del servicio. La respuesta se transmite a medida que llega, así que una que pasa el límite se corta. El estado y las cabeceras ya se enviaron, y el cuerpo queda incompleto. Una consulta de la llamada que nombra X-Sallyport-Call-Id muestra entonces UPSTREAM_TOO_LARGE.