HTTP-Proxy
Senden Sie jede HTTP-Anfrage über Sallyport an einen verbundenen Dienst. Das Gateway setzt den Schlüssel ein, Ihr Skript hält ihn nie.
Auf dieser Seite
Die Adresse
Senden Sie die Anfrage an https://mcp.sallyport.cloud/p/{binding_id}/{path} mit einem Agent-Token in Authorization: Bearer. Sallyport setzt den Schlüssel der Verbindung ein, ruft den Dienst auf und streamt seine Antwort zurück.
Der Dienst erhält https://{host}{path_prefix}, dann den Pfad, den Sie nach der ID geschrieben haben, dann Ihren Query-String. Eine Verbindung aus dem Katalog hat das Präfix /, schreiben Sie also den vollständigen Pfad aus der API-Dokumentation des Anbieters.
Eine Verbindung, die alle Hosts eines Dienstes abdeckt, etwa AWS auf *.amazonaws.com, nimmt den Host als erstes Segment: /p/{binding_id}/sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15.
Was Sie brauchen
- Ein Agent-Token. Öffnen Sie den Agenten unter Agenten und klicken Sie im Abschnitt „Token“ auf „Neuer Token“. Der Wert wird nur einmal angezeigt.
- Die ID einer HTTP-Verbindung. Öffnen Sie Verbindungen und klicken Sie auf die Verbindung: Die ID ist der letzte Teil der Seitenadresse.
- Agent und Verbindung im selben Arbeitsbereich und in derselben Umgebung. Sallyport gibt jeden neuen Schlüssel allen Agenten seines Arbeitsbereichs, das ist also schon so, außer jemand hat es entfernt.
Ihr GitHub-Konto lesen
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"Zurück kommen Status, Header und JSON von GitHub selbst, dazu X-Sallyport-Call-Id mit der ID des Aufrufs. Die Antwort kommt in Teilen ohne Content-Length, und jeder Header kommt an, auch Set-Cookie.
Einen Schreibzugriff senden
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"| Header | Was er tut |
|---|---|
X-Sallyport-Intent | Ein Satz mit höchstens 500 Zeichen, warum der Aufruf nötig ist. Ein Mensch liest ihn auf der Freigabekarte. Ein Aufruf, der ohne ihn auf einen Menschen wartet, erhält 400 mit reason: "missing_intent". |
X-Sallyport-Idempotency-Key | Ihr eigener Schlüssel für einen Schreibzugriff. Eine Wiederholung mit demselben Schlüssel und derselben Anfrage erhält 24 Stunden lang die erste Antwort, statt erneut zu laufen. |
X-Sallyport-Call-Id | Setzt einen Aufruf fort, dessen ID Sie schon haben, egal was sonst in der Anfrage steht. Ein zweiter Aufruf entsteht nicht. |
Diese drei Header leitet Sallyport nie weiter. Außerdem entfernt es Authorization, Cookie, Host, Content-Length, Expect, Forwarded, Via und jeden X-Forwarded-*-Header.
Ein Header an der Stelle des Schlüssels, etwa PRIVATE-TOKEN bei GitLab, wird entfernt. Sein Name landet im Protokoll, und der Aufruf unter Aktivität zeigt seinen Wert, markiert als nicht gesendet. Methodenüberschreibungen wie _method und X-HTTP-Method-Override werden entfernt, also läuft die Methode, die Sie senden.
Wiederholungen
Eine GET-, HEAD- oder OPTIONS-Anfrage läuft immer. Ein Schreibzugriff ohne eigenen Schlüssel bekommt einen, der aus der Anfrage berechnet wird, also sind zwei gleiche Schreibzugriffe ein Aufruf.
Erneut gesendet, während der erste auf einen Menschen wartet, schließt er sich derselben Karte an. Während der erste läuft, erhält er dessen Antwort oder 409 IN_FLIGHT. 2 Minuten nach dem Ende erhält er noch die erste Antwort.
Eine zurückgegebene Antwort trägt X-Sallyport-Deduplicated: true und die X-Sallyport-Call-Id des ersten Aufrufs. Hat der Dienst den Schreibzugriff mit 4xx abgelehnt, wird der Schlüssel frei und die korrigierte Wiederholung läuft. Derselbe Schlüssel mit einer anderen Anfrage erhält 409 IDEMPOTENCY_CONFLICT.
Wenn ein Mensch freigeben muss
Ein Aufruf wartet, wenn die Regel des Agenten für diese Verbindung „fragen“ sagt, etwa nachdem „Bei jedem Aufruf fragen“ eingeschaltet wurde. Die Antwort kommt sofort, mit 202, Retry-After und diesem Body:
{
"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…"
}Zeigen Sie dem Menschen approve_url. Fragen Sie dann nach Retry-After Sekunden poll_url mit demselben Token ab. Jede Methode funktioniert, und nichts läuft doppelt.
| Antwort auf die Abfrage | Bedeutung |
|---|---|
202 mit APPROVAL_PENDING | Noch hat niemand geantwortet. Fragen Sie nach Retry-After erneut. |
| Die Antwort des Dienstes selbst | Der Mensch hat freigegeben. Der Aufruf läuft jetzt, und seine Antwort kommt in dieser Abfrage. |
202 mit state | Der Aufruf ist noch beim Dienst. Fragen Sie in einer Sekunde erneut. |
403 APPROVAL_DENIED | Der Mensch hat abgelehnt. reason und decided_by sagen, warum und wer. Nichts wurde gesendet. |
410 APPROVAL_EXPIRED | Niemand hat rechtzeitig geantwortet. Senden Sie die Anfrage ohne X-Sallyport-Call-Id erneut für eine neue Karte. |
404 CALL_NOT_FOUND | Die call_id gehört einem anderen Agenten oder existiert nicht. Starten Sie einen neuen Aufruf. |
200 mit state und bytes | Der Aufruf ist vorbei und seine Antwort ist schon hinausgegangen. status ist der Status des Dienstes, body_ref ist gesetzt, wenn die Antwort gespeichert wurde. |
error_code mit state und bytes | Der Aufruf endete mit einem Fehler, etwa UPSTREAM_TOO_LARGE. Die Statuszeile ist die dieses Codes, und instruction sagt, was zu tun ist. |
Eine Antwort über 1 MiB wird außerdem gespeichert, standardmäßig 30 Tage. Eine Abfrage des beendeten Aufrufs enthält ihre body_ref, und der Aufruf unter Aktivität bietet „Vollständige Antwort herunterladen“. Laden Sie sie mit GET https://mcp.sallyport.cloud/v1/bodies/{body_ref} und demselben Agent-Token herunter; Range funktioniert.
WebSocket
Öffnen Sie wss://mcp.sallyport.cloud/p/{binding_id}/{path} mit demselben Agent-Token in Authorization: Bearer. Sallyport setzt den Schlüssel der Verbindung dort ein, wo der Dienst ihn liest, bei den meisten Diensten in den Handshake, öffnet wss://{host}{path} beim Dienst und reicht jeden Frame in beide Richtungen so weiter, wie er gesendet wurde: Text, Binärdaten, Ping, Pong und den Close-Code. Das Subprotokoll aus Sec-WebSocket-Protocol erreicht den Dienst, und seine Wahl kommt zurück.
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))Die Sitzung ist ein Aufruf, und über ihn entscheidet wie über jeden anderen die Regel des Agenten für die Verbindung. ElevenLabs-Echtzeit-Transkription ist ein bezahlter Schreibzugriff und läuft direkt durch, außer „Bei jedem Aufruf fragen“ ist eingeschaltet. Der Aufruf unter Aktivität zeigt, wann die Sitzung geöffnet und geschlossen wurde, wer sie mit welchem Code geschlossen hat, was jede Seite gesendet hat, und jede Nachricht in beide Richtungen so, wie sie gesendet wurde. Die Nachrichten werden 30 Tage aufbewahrt, wie eine lange Antwort.
| Limit | Wert | Jenseits des Limits |
|---|---|---|
| Länge der Sitzung | 60 Minuten | Beide Seiten erhalten Close 1001 mit dem Grund. |
| Eine Nachricht, in beide Richtungen | 1 MiB | Beide Seiten erhalten Close 1009. Die Nachricht wird nicht weitergereicht. |
| Stille in beide Richtungen | 10 Minuten ohne Frame | Beide Seiten erhalten Close 1001 mit dem Grund. |
Muss die Sitzung auf einen Menschen warten, bekommt der Handshake 202 und den APPROVAL_PENDING-Body von oben statt 101. Sobald der Mensch freigibt, öffnen Sie den WebSocket erneut mit X-Sallyport-Call-Id: {call_id}, und die freigegebene Sitzung beginnt. Eine Abfrage von poll_url antwortet dann 426 WEBSOCKET_REQUIRED, denn eine Sitzung läuft nur über einen WebSocket. Lehnt der Dienst den Handshake ab, wegen eines falschen Schlüssels oder Parameters, erhalten Sie seinen eigenen Status und Body.
Dienste, die über WebSocket arbeiten
Jeder davon öffnet seine WebSocket-API über /p/, und Sallyport setzt den Schlüssel dort ein, wo der Anbieter ihn liest. Adresse und Nachrichten schreiben Sie so, wie der Anbieter sie dokumentiert. list_access mit service sagt dasselbe für jede Verbindung.
| Dienste | Wo Sallyport den Schlüssel einsetzt |
|---|---|
| OpenAI Realtime, Azure OpenAI Realtime, Gemini Live, xAI, Mistral, Deepgram, AssemblyAI, Speechmatics, Cartesia, MiniMax, Together AI, fal, Murf, Telnyx, ElevenLabs, Retell, Gladia | Im Handshake, wie bei jedem Aufruf |
| Rev.ai, Hume | In der Query: access_token, api_key |
| Alpaca-Marktdaten, CoinGecko, CoinMarketCap, Alchemy | Im Handshake |
| Finnhub, Twelve Data | In der Query: token, apikey |
| Massive, Financial Modeling Prep, Alpaca-Handelsupdates | In der ersten Nachricht, die Sie senden |
| Binance, Bybit, OKX, Kraken, Coinbase, Coinbase Exchange | Nirgends: Die öffentlichen Streams brauchen keinen, und der private Stream von Kraken nimmt das Token aus der Antwort auf GetWebSocketsToken |
| Discord-Gateway | In der ersten Nachricht, die Sie senden: d.token von IDENTIFY |
| Slack Socket Mode, Twitch EventSub, RingCentral, Vapi, Hetzner-Konsole | Nirgends: Öffnen Sie die URL aus der Antwort des Dienstes samt Ticket |
| Railway, Render, Koyeb, Daytona, Mattermost, ntfy, Grafana Cloud | Im Handshake |
| Browserless, Steel, Browserbase | In der Query: token, apiKey |
Manche Dienste betreiben ihren WebSocket auf einem eigenen Host: gateway.discord.gg, agent.deepgram.com, ws.finnhub.io, das wss-….slack.com eines Socket-Mode-Tickets. Einen solchen Host öffnen Sie über die Verbindung dieses Dienstes, als erstes Segment des Pfads: wss://mcp.sallyport.cloud/p/{binding_id}/gateway.discord.gg/?v=10&encoding=json. Nur ein WebSocket erreicht ihn.
Gehört der Schlüssel in die erste Nachricht, senden Sie die Nachricht aus der Dokumentation des Anbieters mit "{{credential}}" an der Stelle des Schlüssels oder ohne dieses Feld. Sallyport schreibt den Schlüssel hinein, bevor die Nachricht den Dienst erreicht, und der Aufruf in der Aktivität zeigt Ihre Nachricht so, wie Sie sie gesendet haben.
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' }))
}Ablehnungen
Eine Ablehnung ist JSON: {"error": {"code": …, "instruction": …}}, daneben die Felder, die ihr Code braucht. instruction ist ein englischer Satz dazu, was als Nächstes zu tun ist.
Vier Codes sind Ergebnisse, keine Fehler, und kommen als einfaches Objekt ohne die Hülle error: APPROVAL_PENDING, ACCESS_REQUEST_CREATED, ALREADY_EXECUTED und UPSTREAM_ERROR. Auf /p/ ist ein UPSTREAM_ERROR-Body die 424-Antwort, wenn der Dienst nie geantwortet hat. Antwortet der Dienst mit einem Fehler, erhalten Sie seine eigene Antwort.
| Status und Code | Was passiert ist |
|---|---|
401 AUTH_FAILED | Das Token fehlt, ist falsch, widerrufen oder nur für /mcp ausgestellt. WWW-Authenticate sagt einem OAuth-Client, wo er das richtige bekommt. |
403 SOURCE_IP_NOT_ALLOWED | Die Anfrage kam von einer Adresse außerhalb der erlaubten Liste des Agenten. |
423 SUSPENDED | Der Agent, sein Arbeitsbereich oder die Organisation ist pausiert. |
404 ACCESS_REQUEST_CREATED | Der Agent kann diese Verbindung nicht nutzen: kein Zugriff, pausierte Verbindung oder unbekannte ID. Zeigen Sie einem Menschen approve_url und wiederholen Sie dann den Aufruf. |
403 POLICY_DENY | Die Regel des Agenten für diese Verbindung lehnt diese Aufrufklasse ab. |
404 CALL_NOT_FOUND | Die X-Sallyport-Call-Id oder der abgefragte Aufruf gehört einem anderen Agenten oder existiert nicht. Starten Sie einen neuen Aufruf. |
400 BAD_REQUEST | reason nennt die Korrektur: missing_intent, intent_too_long, body_too_large mit max_bytes, missing_host, query_in_path, mcp_binding, database_binding, email_binding, email_operation, sftp_operation, websocket_handshake und weitere. |
400 PATH_OUT_OF_PREFIX | Der Pfad verlässt die Pfadgrenze der Verbindung, etwa über ... |
409-Codes | IDEMPOTENCY_CONFLICT, IN_FLIGHT, CALL_CONFLICT oder APPROVAL_SUPERSEDED. Die instruction sagt, was zu tun ist. |
426 UPGRADE_NOT_SUPPORTED | Ein Upgrade auf ein anderes Protokoll als WebSocket. |
426 WEBSOCKET_REQUIRED | Eine Abfrage einer freigegebenen WebSocket-Sitzung. Öffnen Sie den WebSocket erneut mit X-Sallyport-Call-Id. |
429 RATE_LIMITED | Ein Ratenlimit des Agenten oder der Verbindung. Warten Sie Retry-After ab und wiederholen Sie. |
424-Codes | EGRESS_DENIED, SERVICE_NOT_CONNECTED, UPSTREAM_TOO_LARGE, OUTCOME_UNKNOWN oder UPSTREAM_ERROR. Lesen Sie vor einer Wiederholung die instruction: OUTCOME_UNKNOWN heißt, der Dienst hat vielleicht gehandelt. |
503 UNAVAILABLE, 500 INTERNAL | Ein Fehler auf der Seite von Sallyport. Wiederholen Sie ein 503 nach Retry-After. |
Die Anfrage im Body beschreiben
Ein Client, der Methode oder Pfad nicht setzen kann, sendet per POST eine Beschreibung mit Content-Type: application/vnd.sallyport.request+json. Jedes Feld ist optional und ersetzt diesen Teil der Anfrage.
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/"Die Felder sind method, path, query, headers, body, intent und idempotency_key. path nimmt kein ? und kein #, Parameter gehören in query. Ein JSON-body geht als JSON hinaus, ein String als roher Text.
Eine Antwort darf bis 512 MiB groß sein. Die Seite der Verbindung zeigt ihr eigenes Limit unter „Limits“ als „Größte Antwort des Dienstes“. Die Antwort wird gestreamt, sobald sie eintrifft, daher wird eine Antwort über dem Limit abgeschnitten. Status und Header sind dann schon gesendet, und der Body bricht ab. Eine Abfrage des Aufrufs aus X-Sallyport-Call-Id zeigt dann UPSTREAM_TOO_LARGE.