@aerostack/gateway 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -0
- package/dist/index.js +8 -0
- package/dist/logger.js +2 -0
- package/dist/resolution.js +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @aerostack/gateway
|
|
2
|
+
|
|
3
|
+
Connect any MCP client (Claude Desktop, Cline, Cursor, Goose, Windsurf, and more) to an [Aerostack Workspace](https://aerostack.dev) — one URL, all your tools, with built-in human approval gates.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.npmjs.com/package/@aerostack/gateway)
|
|
7
|
+
|
|
8
|
+
## Why?
|
|
9
|
+
|
|
10
|
+
Many MCP clients only support **stdio** transport. Aerostack Workspaces use **HTTP**. This bridge connects them — no code required.
|
|
11
|
+
|
|
12
|
+
Instead of configuring dozens of separate MCP servers in your AI tool, point at one Aerostack Workspace and get:
|
|
13
|
+
|
|
14
|
+
- **All tools in one place** — composed from multiple MCP servers
|
|
15
|
+
- **Centralized OAuth** — 27+ providers managed by Aerostack, not your agent
|
|
16
|
+
- **Human approval gates** — sensitive tools require your approval before executing
|
|
17
|
+
- **Local Guardian** — approval gates for local operations (file delete, shell, git push, deploy)
|
|
18
|
+
- **Per-tool permissions** — workspace tokens scope exactly which tools are accessible
|
|
19
|
+
- **Usage tracking & audit** — every tool call logged and metered
|
|
20
|
+
- **Rate limiting** — per-token, plan-tiered
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Get a workspace token
|
|
25
|
+
|
|
26
|
+
In your [Aerostack Admin Dashboard](https://app.aerostack.dev), open your workspace and generate a token (`mwt_...`).
|
|
27
|
+
|
|
28
|
+
### 2. Configure your AI tool
|
|
29
|
+
|
|
30
|
+
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"aerostack": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "@aerostack/gateway"],
|
|
38
|
+
"env": {
|
|
39
|
+
"AEROSTACK_WORKSPACE_URL": "https://api.aerostack.dev/api/gateway/ws/my-workspace",
|
|
40
|
+
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Cline (VS Code)** — add to VS Code settings under `cline.mcpServers`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"aerostack": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@aerostack/gateway"],
|
|
54
|
+
"env": {
|
|
55
|
+
"AEROSTACK_WORKSPACE_URL": "https://api.aerostack.dev/api/gateway/ws/my-workspace",
|
|
56
|
+
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Cursor / Goose / HTTP-native clients** — use the workspace URL directly (no bridge needed):
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
URL: https://api.aerostack.dev/api/gateway/ws/my-workspace
|
|
66
|
+
Token: mwt_your_token_here (Authorization: Bearer header)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Use it
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
You: Create a GitHub issue for the login bug
|
|
73
|
+
AI: [calls github__create_issue via Aerostack workspace]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Tools are namespaced as `{server}__{tool}` (e.g., `github__create_issue`, `slack__send_message`).
|
|
77
|
+
|
|
78
|
+
## How It Works
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Your AI tool (stdio) → @aerostack/gateway → HTTPS → Aerostack Workspace
|
|
82
|
+
↓
|
|
83
|
+
┌────────────────────────┐
|
|
84
|
+
│ Fan-out to MCP servers │
|
|
85
|
+
│ GitHub, Slack, Gmail, │
|
|
86
|
+
│ custom Workers, etc. │
|
|
87
|
+
└────────────────────────┘
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The bridge:
|
|
91
|
+
|
|
92
|
+
1. Starts as a stdio MCP server (what your AI tool expects)
|
|
93
|
+
2. Forwards all JSON-RPC requests to your workspace over HTTPS
|
|
94
|
+
3. Handles SSE streaming responses transparently
|
|
95
|
+
4. Manages the approval gate flow automatically
|
|
96
|
+
|
|
97
|
+
## Approval Gates
|
|
98
|
+
|
|
99
|
+
When your workspace has approval rules configured, the bridge handles them transparently:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
AI calls dangerous_tool
|
|
103
|
+
→ Bridge forwards to workspace
|
|
104
|
+
→ Workspace returns "needs approval" (-32050)
|
|
105
|
+
→ Bridge waits via WebSocket (instant) or polls as fallback
|
|
106
|
+
→ You approve/reject in dashboard or mobile app
|
|
107
|
+
→ Bridge retries on approval, returns error on rejection
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Your AI agent sees either a successful result or a clear error — no special handling needed.
|
|
111
|
+
|
|
112
|
+
## Local Guardian
|
|
113
|
+
|
|
114
|
+
Approval gates for **local operations** — file deletion, destructive shell commands, git push, deploys. The agent asks for approval before acting on your machine.
|
|
115
|
+
|
|
116
|
+
The bridge injects an `aerostack__local_guardian` tool. When the LLM is about to perform a risky local operation, it calls this tool first. You get a push notification and can approve or reject from the dashboard or your phone.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Agent wants to: rm -rf ./old-config/
|
|
120
|
+
→ Agent calls aerostack__local_guardian
|
|
121
|
+
→ You get push notification on your phone
|
|
122
|
+
→ Tap Approve or Reject
|
|
123
|
+
→ Agent proceeds or stops
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Category | What it covers |
|
|
127
|
+
|----------|---------------|
|
|
128
|
+
| `file_delete` | Deleting or overwriting files |
|
|
129
|
+
| `shell_destructive` | `rm -rf`, `DROP TABLE`, etc. |
|
|
130
|
+
| `git_push` | `git push`, force push, `git reset --hard` |
|
|
131
|
+
| `config_modify` | `.env`, credentials, production configs |
|
|
132
|
+
| `deploy` | Deploy, publish, release to any environment |
|
|
133
|
+
| `database` | Direct database mutations outside migrations |
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
| Variable | Required | Default | Description |
|
|
138
|
+
|----------|----------|---------|-------------|
|
|
139
|
+
| `AEROSTACK_WORKSPACE_URL` | Yes | — | Full workspace endpoint URL |
|
|
140
|
+
| `AEROSTACK_TOKEN` | Yes | — | Workspace token (`mwt_...`) |
|
|
141
|
+
| `AEROSTACK_APPROVAL_POLL_MS` | No | `3000` | Approval polling interval (ms) |
|
|
142
|
+
| `AEROSTACK_APPROVAL_TIMEOUT_MS` | No | `300000` | Max approval wait time (5 min) |
|
|
143
|
+
| `AEROSTACK_REQUEST_TIMEOUT_MS` | No | `30000` | HTTP request timeout (30s) |
|
|
144
|
+
| `AEROSTACK_LOCAL_GUARDIAN` | No | `true` | Set `false` to disable Local Guardian |
|
|
145
|
+
| `AEROSTACK_LOG_LEVEL` | No | `info` | Log level: debug, info, warn, error |
|
|
146
|
+
|
|
147
|
+
## Supported MCP Methods
|
|
148
|
+
|
|
149
|
+
| Method | Description |
|
|
150
|
+
|--------|-------------|
|
|
151
|
+
| `tools/list` | List all tools from all workspace servers |
|
|
152
|
+
| `tools/call` | Call a namespaced tool (with approval handling) |
|
|
153
|
+
| `resources/list` | List resources from all workspace servers |
|
|
154
|
+
| `resources/read` | Read a specific resource |
|
|
155
|
+
| `prompts/list` | List prompts from all workspace servers |
|
|
156
|
+
| `prompts/get` | Get a prompt with arguments |
|
|
157
|
+
|
|
158
|
+
## Requirements
|
|
159
|
+
|
|
160
|
+
- Node.js 18+
|
|
161
|
+
- An Aerostack account with a configured workspace
|
|
162
|
+
- A workspace token (`mwt_...`)
|
|
163
|
+
|
|
164
|
+
## Links
|
|
165
|
+
|
|
166
|
+
- [Aerostack Docs](https://docs.aerostack.dev)
|
|
167
|
+
- [Aerostack Admin](https://app.aerostack.dev)
|
|
168
|
+
- [MCP Specification](https://modelcontextprotocol.io)
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import{Server as v}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as O,CallToolRequestSchema as h,ListResourcesRequestSchema as y,ReadResourceRequestSchema as P,ListPromptsRequestSchema as U,GetPromptRequestSchema as q}from"@modelcontextprotocol/sdk/types.js";import{resolveApproval as E}from"./resolution.js";import{info as m,error as C}from"./logger.js";const R=process.env.AEROSTACK_WORKSPACE_URL,f=process.env.AEROSTACK_TOKEN;function g(e,r,t){const o=parseInt(e??String(r),10);return Number.isFinite(o)&&o>=t?o:r}const T=g(process.env.AEROSTACK_APPROVAL_POLL_MS,3e3,500),S=g(process.env.AEROSTACK_APPROVAL_TIMEOUT_MS,3e5,5e3),L=g(process.env.AEROSTACK_REQUEST_TIMEOUT_MS,3e4,1e3);R||(process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL is required
|
|
4
|
+
`),process.exit(1)),f||(process.stderr.write(`ERROR: AEROSTACK_TOKEN is required
|
|
5
|
+
`),process.exit(1));let d;try{if(d=new URL(R),d.protocol!=="https:"&&d.protocol!=="http:")throw new Error("must be http or https")}catch{process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL must be a valid HTTP(S) URL
|
|
6
|
+
`),process.exit(1)}d.protocol==="http:"&&!d.hostname.match(/^(localhost|127\.0\.0\.1)$/)&&process.stderr.write(`WARNING: Using HTTP (not HTTPS) \u2014 token will be sent in plaintext
|
|
7
|
+
`);const w=R.replace(/\/+$/,"");async function c(e,r){const t={jsonrpc:"2.0",id:Date.now(),method:e,params:r??{}},o=new AbortController,n=setTimeout(()=>o.abort(),L);try{const s=await fetch(w,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${f}`,"User-Agent":"aerostack-gateway/0.12.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify(t),signal:o.signal});if(clearTimeout(n),(s.headers.get("content-type")??"").includes("text/event-stream")){const i=await s.text();return j(i,t.id)}return await s.json()}catch(s){clearTimeout(n);const a=s instanceof Error?s.message:"Unknown error";return s instanceof Error&&s.name==="AbortError"?{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:"Request timed out"}}:{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:`HTTP error: ${a}`}}}}function j(e,r){const t=e.split(`
|
|
8
|
+
`);let o=null;for(const n of t)if(n.startsWith("data: "))try{o=JSON.parse(n.slice(6))}catch{}return o??{jsonrpc:"2.0",id:r,error:{code:-32603,message:"Empty SSE response"}}}async function x(e,r){const t=await c("tools/call",{name:e,arguments:r});if(t.error?.code===-32050){const s=t.error.data,a=s?.approval_id;if(!a||!/^[a-zA-Z0-9_-]{4,128}$/.test(a))return{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:"Approval required but no approval_id returned"}};m("Tool gate: waiting for approval",{approvalId:a,transport:s?.ws_url?"ws":"poll"});const l=s?.polling_url??`${w}/approval-status/${a}`,i=await E({approvalId:a,wsUrl:s?.ws_url,pollUrl:l,pollIntervalMs:T,timeoutMs:S});return i.status==="rejected"?{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:`Tool call rejected: ${i.reviewer_note??"no reason given"}`}}:i.status==="expired"?{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:"Approval request expired"}}:(m("Retrying tool call after approval",{approvalId:a,status:i.status}),c("tools/call",{name:e,arguments:r}))}const n=t.result?._meta;if(n?.approval_id&&n?.status==="pending"){const s=n.approval_id;m("Permission gate: waiting for approval",{approvalId:s,transport:n.ws_url?"ws":"poll"});const a=n.polling_url??`${w}/approval-status/${s}`,l=await E({approvalId:s,wsUrl:n.ws_url,pollUrl:a,pollIntervalMs:T,timeoutMs:S});let i;return l.status==="approved"||l.status==="executed"?i="APPROVED \u2014 Your request has been approved. You may proceed with the action.":l.status==="rejected"?i=`REJECTED \u2014 Your request was denied. Reason: ${l.reviewer_note??"No reason given."}. Do NOT proceed.`:i="EXPIRED \u2014 Your approval request timed out. Submit a new request if needed.",{jsonrpc:"2.0",id:t.id,result:{content:[{type:"text",text:i}]}}}return t}let A=null;async function u(){if(A)return;const e=await c("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"aerostack-gateway",version:"0.12.0"}});if(e.result){const r=e.result;A={protocolVersion:r.protocolVersion??"2024-11-05",instructions:r.instructions}}}const p=new v({name:"aerostack-gateway",version:"0.12.0"},{capabilities:{tools:{},resources:{},prompts:{}}});p.setRequestHandler(O,async()=>{await u();const e=await c("tools/list");if(e.error)throw new Error(e.error.message);return{tools:e.result.tools??[]}}),p.setRequestHandler(h,async e=>{await u();const{name:r,arguments:t}=e.params,o=await x(r,t??{});return o.error?{content:[{type:"text",text:`Error: ${o.error.message}`}],isError:!0}:{content:o.result.content??[{type:"text",text:JSON.stringify(o.result)}]}}),p.setRequestHandler(y,async()=>{await u();const e=await c("resources/list");if(e.error)throw new Error(e.error.message);return{resources:e.result.resources??[]}}),p.setRequestHandler(P,async e=>{await u();const r=await c("resources/read",{uri:e.params.uri});if(r.error)throw new Error(r.error.message);return{contents:r.result.contents??[]}}),p.setRequestHandler(U,async()=>{await u();const e=await c("prompts/list");if(e.error)throw new Error(e.error.message);return{prompts:e.result.prompts??[]}}),p.setRequestHandler(q,async e=>{await u();const r=await c("prompts/get",{name:e.params.name,arguments:e.params.arguments});if(r.error)throw new Error(r.error.message);return{messages:r.result.messages??[]}});async function I(){m("Connecting to workspace",{url:w});const e=new _;await p.connect(e),m("Ready",{url:w})}I().catch(e=>{C("Fatal error",{error:e instanceof Error?e.message:String(e)}),process.exit(1)});
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const t={debug:0,info:1,warn:2,error:3},i=(()=>{const r=(process.env.AEROSTACK_LOG_LEVEL??"info").toLowerCase();return r in t?r:"info"})();function n(r,o,e){if(t[r]<t[i])return;const s={ts:new Date().toISOString(),level:r,msg:o,...e};process.stderr.write(JSON.stringify(s)+`
|
|
2
|
+
`)}const c=(r,o)=>n("debug",r,o),f=(r,o)=>n("info",r,o),g=(r,o)=>n("warn",r,o),p=(r,o)=>n("error",r,o);export{c as debug,p as error,f as info,n as log,g as warn};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{info as u,warn as g,debug as b}from"./logger.js";async function j(e){const{approvalId:r,wsUrl:n,pollUrl:o,pollIntervalMs:l,timeoutMs:k}=e,d=Date.now()+k;if(n)try{return await A(r,n,o,d)}catch(c){const f=c instanceof Error?c.message:"Unknown WS error";g("WebSocket resolution failed, falling back to polling",{approvalId:r,error:f})}return O(r,o,l,d)}async function A(e,r,n,o){const l=await E();return new Promise((k,d)=>{let c=!1,f=null,m=null;const y=()=>{c=!0,f&&clearInterval(f),m&&clearTimeout(m)},w=t=>{c||(y(),k(t))},p=t=>{c||(y(),d(t))};b("Connecting WebSocket",{approvalId:e,wsUrl:r});const a=new l(r);a.onopen=()=>{u("WebSocket connected, waiting for resolution",{approvalId:e})},a.onmessage=t=>{try{const i=typeof t.data=="string"?JSON.parse(t.data):JSON.parse(String(t.data)),s=i?.status;if(b("WebSocket message received",{approvalId:e,status:s}),s==="executed"||s==="approved"||s==="rejected"||s==="expired"){u("Approval resolved via WebSocket",{approvalId:e,status:s}),w({status:s,reviewer_note:i?.reviewer_note});try{a.close(1e3)}catch{}}}catch{g("Failed to parse WebSocket message",{approvalId:e})}},a.onerror=t=>{const i=t instanceof Error?t.message:"WebSocket error";b("WebSocket error",{approvalId:e,error:i}),p(new Error(`WebSocket error: ${i}`))},a.onclose=t=>{c||(b("WebSocket closed without resolution",{approvalId:e,code:t.code}),p(new Error(`WebSocket closed unexpectedly (code ${t.code})`)))},typeof a.on=="function"&&a.on("unexpected-response",async(t,i)=>{try{const s=[];for await(const x of i)s.push(x);const _=Buffer.concat(s).toString(),v=JSON.parse(_),S=v?.status;if(S&&S!=="pending"){u("Approval already resolved (WS endpoint returned JSON)",{approvalId:e,status:S}),w({status:S,reviewer_note:v?.reviewer_note});return}}catch{}p(new Error("WebSocket upgrade rejected by server"))}),f=setInterval(async()=>{if(!c)try{const t=await h(n);if(t){u("Approval resolved via safety-net poll",{approvalId:e,status:t.status}),w(t);try{a.close(1e3)}catch{}}}catch{}},3e4);const W=o-Date.now();if(W<=0){p(new Error("Approval timeout"));return}m=setTimeout(()=>{if(!c){g("Approval timed out",{approvalId:e}),w({status:"expired"});try{a.close(1e3)}catch{}}},W)})}async function O(e,r,n,o){for(u("Polling for approval resolution",{approvalId:e,intervalMs:n});Date.now()<o;){await P(n);try{const l=await h(r);if(l)return u("Approval resolved via polling",{approvalId:e,status:l.status}),l}catch{}}return g("Approval polling timed out",{approvalId:e}),{status:"expired"}}async function h(e){const r=await fetch(e,{headers:{"User-Agent":"aerostack-gateway/0.12.0"}});if(!r.ok)return null;const n=await r.json(),o=n.status;return o==="executed"||o==="approved"?{status:o,reviewer_note:n.reviewer_note}:o==="rejected"?{status:"rejected",reviewer_note:n.reviewer_note}:o==="expired"?{status:"expired"}:null}async function E(){if(typeof globalThis.WebSocket<"u")return globalThis.WebSocket;try{return(await import("ws")).default}catch{throw new Error('WebSocket not available. Install the "ws" package: npm install ws')}}function P(e){return new Promise(r=>setTimeout(r,e))}export{j as resolveApproval};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aerostack/gateway",
|
|
3
|
+
"version": "0.11.2",
|
|
4
|
+
"description": "stdio-to-HTTP bridge connecting any MCP client to Aerostack Workspaces",
|
|
5
|
+
"author": "Aerostack",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"aerostack-gateway": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**/*.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "node scripts/build.mjs",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
23
|
+
"ws": "^8.18.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"@types/ws": "^8.5.13",
|
|
28
|
+
"esbuild": "^0.21.5",
|
|
29
|
+
"typescript": "~5.8.3"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"aerostack",
|
|
36
|
+
"mcp",
|
|
37
|
+
"model-context-protocol",
|
|
38
|
+
"gateway",
|
|
39
|
+
"bridge",
|
|
40
|
+
"stdio",
|
|
41
|
+
"claude",
|
|
42
|
+
"cursor",
|
|
43
|
+
"cline",
|
|
44
|
+
"ai-agent"
|
|
45
|
+
]
|
|
46
|
+
}
|