@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 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
+ [![License: MIT](https://img.shields.io/badge/LICENSE_//_MIT-3b5bdb?style=for-the-badge&labelColor=eff6ff)](https://opensource.org/licenses/MIT)
6
+ [![npm](https://img.shields.io/npm/v/@aerostack/gateway?style=for-the-badge&color=3b5bdb&labelColor=eff6ff)](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
+ }