@opengsd/cloud-mcp-gateway 1.2.0
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 +72 -0
- package/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/dist/auth-store.d.ts +62 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +171 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +86 -0
- package/dist/mcp.js.map +1 -0
- package/dist/protocol.d.ts +50 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -0
- package/dist/runtime-registry.d.ts +27 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +198 -0
- package/dist/runtime-registry.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -0
- package/dist/server.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @opengsd/cloud-mcp-gateway
|
|
2
|
+
|
|
3
|
+
Cloud-hosted MCP gateway for brokering remote MCP clients to a paired Local GSD Runtime.
|
|
4
|
+
|
|
5
|
+
The gateway is a live routing layer. It does not host workspaces, clone source code, store `.gsd` artifacts, or run GSD workflows itself.
|
|
6
|
+
|
|
7
|
+
## Local Smoke Test
|
|
8
|
+
|
|
9
|
+
Build and start the gateway with persistent auth storage:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
export GSD_CLOUD_USER_TOKEN="$(openssl rand -hex 32)"
|
|
13
|
+
npm run build -w @opengsd/cloud-mcp-gateway
|
|
14
|
+
node packages/cloud-mcp-gateway/dist/cli.js \
|
|
15
|
+
--port 8787 \
|
|
16
|
+
--auth-store ./.tmp/gsd-cloud-auth.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Create a pairing code:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl -s -X POST http://localhost:8787/pairing-codes \
|
|
23
|
+
-H "Authorization: Bearer $GSD_CLOUD_USER_TOKEN" \
|
|
24
|
+
-H 'Content-Type: application/json'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Pair and connect the local daemon with the returned code:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm run build -w @opengsd/daemon
|
|
31
|
+
node packages/daemon/dist/cli.js cloud pair \
|
|
32
|
+
--gateway http://localhost:8787 \
|
|
33
|
+
--code <CODE> \
|
|
34
|
+
--runtime-name local-dev
|
|
35
|
+
|
|
36
|
+
node packages/daemon/dist/cli.js cloud connect --verbose
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
List projects through MCP:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node --input-type=module <<'NODE'
|
|
43
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
44
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
45
|
+
|
|
46
|
+
const client = new Client({ name: "gateway-smoke", version: "0.0.1" });
|
|
47
|
+
const transport = new StreamableHTTPClientTransport(
|
|
48
|
+
new URL("http://localhost:8787/mcp"),
|
|
49
|
+
{ requestInit: { headers: { Authorization: `Bearer ${process.env.GSD_CLOUD_USER_TOKEN}` } } },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await client.connect(transport);
|
|
53
|
+
const result = await client.callTool({ name: "gsd_cloud_projects", arguments: {} });
|
|
54
|
+
console.log(result.content[0].text);
|
|
55
|
+
await client.close();
|
|
56
|
+
NODE
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Auth Storage
|
|
60
|
+
|
|
61
|
+
By default, the gateway uses in-memory auth state for local development and tests.
|
|
62
|
+
|
|
63
|
+
For persistent auth state, set one of:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node packages/cloud-mcp-gateway/dist/cli.js --auth-store /secure/path/gsd-cloud-auth.json
|
|
67
|
+
GSD_CLOUD_AUTH_STORE_PATH=/secure/path/gsd-cloud-auth.json node packages/cloud-mcp-gateway/dist/cli.js
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The auth store persists user tokens, device tokens, and pairing codes as salted scrypt-derived hashes. Raw bearer tokens and device tokens are not written to disk.
|
|
71
|
+
|
|
72
|
+
`GSD_CLOUD_USER_TOKEN` seeds the initial user bearer token and is required at startup.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const target = join(binDir, '..', 'dist', 'cli.js');
|
|
8
|
+
|
|
9
|
+
if (!existsSync(target)) {
|
|
10
|
+
process.stderr.write('gsd-cloud-mcp-gateway: build output missing. Run `pnpm --filter @opengsd/cloud-mcp-gateway run build`.\n');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await import('../dist/cli.js');
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface UserTokenRecord {
|
|
2
|
+
userId: string;
|
|
3
|
+
revoked?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface DeviceTokenRecord {
|
|
6
|
+
userId: string;
|
|
7
|
+
runtimeId: string;
|
|
8
|
+
runtimeName?: string;
|
|
9
|
+
revoked?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface PairingCodeRecord {
|
|
12
|
+
userId: string;
|
|
13
|
+
expiresAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DeviceTokenIssue {
|
|
16
|
+
userId: string;
|
|
17
|
+
runtimeId: string;
|
|
18
|
+
deviceToken: string;
|
|
19
|
+
}
|
|
20
|
+
export interface AuthStoreSnapshot {
|
|
21
|
+
version: 1;
|
|
22
|
+
userTokens: Array<UserTokenRecord & SecretHashRecord>;
|
|
23
|
+
deviceTokens: Array<DeviceTokenRecord & SecretHashRecord>;
|
|
24
|
+
pairingCodes: Array<PairingCodeRecord & SecretHashRecord>;
|
|
25
|
+
}
|
|
26
|
+
export interface SecretHashRecord {
|
|
27
|
+
secretHash: string;
|
|
28
|
+
secretSalt: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class InMemoryAuthStore {
|
|
31
|
+
protected readonly userTokens: Map<string, UserTokenRecord & SecretHashRecord>;
|
|
32
|
+
protected readonly deviceTokens: Map<string, DeviceTokenRecord & SecretHashRecord>;
|
|
33
|
+
protected readonly pairingCodes: Map<string, PairingCodeRecord & SecretHashRecord>;
|
|
34
|
+
constructor(seedUserToken?: {
|
|
35
|
+
token: string;
|
|
36
|
+
userId: string;
|
|
37
|
+
}, snapshot?: AuthStoreSnapshot);
|
|
38
|
+
addUserToken(token: string, userId: string): void;
|
|
39
|
+
authenticateUser(token: string | undefined): string | null;
|
|
40
|
+
authenticateDevice(token: string | undefined): DeviceTokenRecord | null;
|
|
41
|
+
createPairingCode(userId: string, ttlMs?: number): {
|
|
42
|
+
code: string;
|
|
43
|
+
expiresAt: number;
|
|
44
|
+
};
|
|
45
|
+
exchangePairingCode(code: string, runtimeName?: string): DeviceTokenIssue;
|
|
46
|
+
revokeDeviceToken(deviceToken: string): boolean;
|
|
47
|
+
snapshot(): AuthStoreSnapshot;
|
|
48
|
+
protected afterMutation(): void;
|
|
49
|
+
private loadSnapshot;
|
|
50
|
+
}
|
|
51
|
+
export declare class FileAuthStore extends InMemoryAuthStore {
|
|
52
|
+
private readonly filePath;
|
|
53
|
+
constructor(filePath: string, seedUserToken?: {
|
|
54
|
+
token: string;
|
|
55
|
+
userId: string;
|
|
56
|
+
});
|
|
57
|
+
protected afterMutation(): void;
|
|
58
|
+
private persist;
|
|
59
|
+
}
|
|
60
|
+
export declare function extractBearerToken(header: string | string[] | undefined): string | undefined;
|
|
61
|
+
export declare function deriveSecretHash(secret: string, secretSalt?: string): SecretHashRecord;
|
|
62
|
+
//# sourceMappingURL=auth-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,KAAK,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC;IACtD,YAAY,EAAE,KAAK,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,CAAC;IAC1D,YAAY,EAAE,KAAK,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAiB;IAC5B,SAAS,CAAC,QAAQ,CAAC,UAAU,kDAAyD;IACtF,SAAS,CAAC,QAAQ,CAAC,YAAY,oDAA2D;IAC1F,SAAS,CAAC,QAAQ,CAAC,YAAY,oDAA2D;gBAE9E,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,CAAC,EAAE,iBAAiB;IAK3F,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAQjD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAO1D,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,GAAG,IAAI;IAOvE,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAiB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAS9F,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB;IAkBzE,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAQ/C,QAAQ,IAAI,iBAAiB;IAS7B,SAAS,CAAC,aAAa,IAAI,IAAI;IAI/B,OAAO,CAAC,YAAY;CAarB;AAED,qBAAa,aAAc,SAAQ,iBAAiB;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAGhC,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;cAQhC,aAAa,IAAI,IAAI;IAIxC,OAAO,CAAC,OAAO;CAMhB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB5F;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,SAAkC,GAAG,gBAAgB,CAK/G"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
export class InMemoryAuthStore {
|
|
5
|
+
userTokens = new Map();
|
|
6
|
+
deviceTokens = new Map();
|
|
7
|
+
pairingCodes = new Map();
|
|
8
|
+
constructor(seedUserToken, snapshot) {
|
|
9
|
+
if (snapshot)
|
|
10
|
+
this.loadSnapshot(snapshot);
|
|
11
|
+
if (seedUserToken)
|
|
12
|
+
this.addUserToken(seedUserToken.token, seedUserToken.userId);
|
|
13
|
+
}
|
|
14
|
+
addUserToken(token, userId) {
|
|
15
|
+
const existing = findSecretRecord(this.userTokens, token);
|
|
16
|
+
if (existing?.userId === userId && !existing.revoked)
|
|
17
|
+
return;
|
|
18
|
+
const key = deriveSecretHash(token);
|
|
19
|
+
this.userTokens.set(key.secretHash, { ...key, userId });
|
|
20
|
+
this.afterMutation();
|
|
21
|
+
}
|
|
22
|
+
authenticateUser(token) {
|
|
23
|
+
if (!token)
|
|
24
|
+
return null;
|
|
25
|
+
const record = findSecretRecord(this.userTokens, token);
|
|
26
|
+
if (!record || record.revoked)
|
|
27
|
+
return null;
|
|
28
|
+
return record.userId;
|
|
29
|
+
}
|
|
30
|
+
authenticateDevice(token) {
|
|
31
|
+
if (!token)
|
|
32
|
+
return null;
|
|
33
|
+
const record = findSecretRecord(this.deviceTokens, token);
|
|
34
|
+
if (!record || record.revoked)
|
|
35
|
+
return null;
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
38
|
+
createPairingCode(userId, ttlMs = 10 * 60 * 1000) {
|
|
39
|
+
const code = randomBytes(4).toString("hex").toUpperCase();
|
|
40
|
+
const expiresAt = Date.now() + ttlMs;
|
|
41
|
+
const key = deriveSecretHash(code);
|
|
42
|
+
this.pairingCodes.set(key.secretHash, { ...key, userId, expiresAt });
|
|
43
|
+
this.afterMutation();
|
|
44
|
+
return { code, expiresAt };
|
|
45
|
+
}
|
|
46
|
+
exchangePairingCode(code, runtimeName) {
|
|
47
|
+
const normalized = code.trim().toUpperCase();
|
|
48
|
+
const codeEntry = findSecretEntry(this.pairingCodes, normalized);
|
|
49
|
+
if (!codeEntry || codeEntry[1].expiresAt < Date.now()) {
|
|
50
|
+
if (codeEntry)
|
|
51
|
+
this.pairingCodes.delete(codeEntry[0]);
|
|
52
|
+
this.afterMutation();
|
|
53
|
+
throw new Error("Pairing code is invalid or expired");
|
|
54
|
+
}
|
|
55
|
+
const [codeHash, record] = codeEntry;
|
|
56
|
+
this.pairingCodes.delete(codeHash);
|
|
57
|
+
const runtimeId = `rt_${randomUUID()}`;
|
|
58
|
+
const deviceToken = `gsd_dev_${randomBytes(32).toString("hex")}`;
|
|
59
|
+
const key = deriveSecretHash(deviceToken);
|
|
60
|
+
this.deviceTokens.set(key.secretHash, { ...key, userId: record.userId, runtimeId, runtimeName });
|
|
61
|
+
this.afterMutation();
|
|
62
|
+
return { userId: record.userId, runtimeId, deviceToken };
|
|
63
|
+
}
|
|
64
|
+
revokeDeviceToken(deviceToken) {
|
|
65
|
+
const record = findSecretRecord(this.deviceTokens, deviceToken);
|
|
66
|
+
if (!record)
|
|
67
|
+
return false;
|
|
68
|
+
record.revoked = true;
|
|
69
|
+
this.afterMutation();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
snapshot() {
|
|
73
|
+
return {
|
|
74
|
+
version: 1,
|
|
75
|
+
userTokens: Array.from(this.userTokens.values()),
|
|
76
|
+
deviceTokens: Array.from(this.deviceTokens.values()),
|
|
77
|
+
pairingCodes: Array.from(this.pairingCodes.values()),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
afterMutation() {
|
|
81
|
+
// Extension point for persistent stores.
|
|
82
|
+
}
|
|
83
|
+
loadSnapshot(snapshot) {
|
|
84
|
+
for (const record of snapshot.userTokens ?? []) {
|
|
85
|
+
this.userTokens.set(record.secretHash, record);
|
|
86
|
+
}
|
|
87
|
+
for (const record of snapshot.deviceTokens ?? []) {
|
|
88
|
+
this.deviceTokens.set(record.secretHash, record);
|
|
89
|
+
}
|
|
90
|
+
for (const record of snapshot.pairingCodes ?? []) {
|
|
91
|
+
if (record.expiresAt >= Date.now()) {
|
|
92
|
+
this.pairingCodes.set(record.secretHash, record);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export class FileAuthStore extends InMemoryAuthStore {
|
|
98
|
+
filePath;
|
|
99
|
+
constructor(filePath, seedUserToken) {
|
|
100
|
+
super(undefined, readSnapshot(filePath));
|
|
101
|
+
this.filePath = filePath;
|
|
102
|
+
if (seedUserToken)
|
|
103
|
+
this.addUserToken(seedUserToken.token, seedUserToken.userId);
|
|
104
|
+
this.persist();
|
|
105
|
+
}
|
|
106
|
+
afterMutation() {
|
|
107
|
+
this.persist();
|
|
108
|
+
}
|
|
109
|
+
persist() {
|
|
110
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
111
|
+
const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
112
|
+
writeFileSync(tmp, `${JSON.stringify(this.snapshot(), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
113
|
+
renameSync(tmp, this.filePath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function extractBearerToken(header) {
|
|
117
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
118
|
+
if (!value || value.length <= "Bearer ".length)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (value.slice(0, "Bearer".length).toLowerCase() !== "bearer")
|
|
121
|
+
return undefined;
|
|
122
|
+
const firstSeparator = value.charCodeAt("Bearer".length);
|
|
123
|
+
if (firstSeparator !== 0x20 && firstSeparator !== 0x09)
|
|
124
|
+
return undefined;
|
|
125
|
+
let tokenStart = "Bearer".length + 1;
|
|
126
|
+
while (tokenStart < value.length) {
|
|
127
|
+
const char = value.charCodeAt(tokenStart);
|
|
128
|
+
if (char !== 0x20 && char !== 0x09)
|
|
129
|
+
break;
|
|
130
|
+
tokenStart += 1;
|
|
131
|
+
}
|
|
132
|
+
return tokenStart < value.length ? value.slice(tokenStart) : undefined;
|
|
133
|
+
}
|
|
134
|
+
export function deriveSecretHash(secret, secretSalt = randomBytes(16).toString("hex")) {
|
|
135
|
+
return {
|
|
136
|
+
secretHash: scryptSync(secret, secretSalt, 32).toString("hex"),
|
|
137
|
+
secretSalt,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function findSecretRecord(records, secret) {
|
|
141
|
+
return findSecretEntry(records, secret)?.[1];
|
|
142
|
+
}
|
|
143
|
+
function findSecretEntry(records, secret) {
|
|
144
|
+
for (const entry of records) {
|
|
145
|
+
if (secretMatches(entry[1], secret))
|
|
146
|
+
return entry;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
function secretMatches(record, secret) {
|
|
151
|
+
const candidate = Buffer.from(deriveSecretHash(secret, record.secretSalt).secretHash, "hex");
|
|
152
|
+
const expected = Buffer.from(record.secretHash, "hex");
|
|
153
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
154
|
+
}
|
|
155
|
+
function readSnapshot(filePath) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
158
|
+
if (parsed.version !== 1)
|
|
159
|
+
return undefined;
|
|
160
|
+
return {
|
|
161
|
+
version: 1,
|
|
162
|
+
userTokens: Array.isArray(parsed.userTokens) ? parsed.userTokens : [],
|
|
163
|
+
deviceTokens: Array.isArray(parsed.deviceTokens) ? parsed.deviceTokens : [],
|
|
164
|
+
pairingCodes: Array.isArray(parsed.pairingCodes) ? parsed.pairingCodes : [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=auth-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from \"node:crypto\";\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface UserTokenRecord {\n userId: string;\n revoked?: boolean;\n}\n\nexport interface DeviceTokenRecord {\n userId: string;\n runtimeId: string;\n runtimeName?: string;\n revoked?: boolean;\n}\n\nexport interface PairingCodeRecord {\n userId: string;\n expiresAt: number;\n}\n\nexport interface DeviceTokenIssue {\n userId: string;\n runtimeId: string;\n deviceToken: string;\n}\n\nexport interface AuthStoreSnapshot {\n version: 1;\n userTokens: Array<UserTokenRecord & SecretHashRecord>;\n deviceTokens: Array<DeviceTokenRecord & SecretHashRecord>;\n pairingCodes: Array<PairingCodeRecord & SecretHashRecord>;\n}\n\nexport interface SecretHashRecord {\n secretHash: string;\n secretSalt: string;\n}\n\nexport class InMemoryAuthStore {\n protected readonly userTokens = new Map<string, UserTokenRecord & SecretHashRecord>();\n protected readonly deviceTokens = new Map<string, DeviceTokenRecord & SecretHashRecord>();\n protected readonly pairingCodes = new Map<string, PairingCodeRecord & SecretHashRecord>();\n\n constructor(seedUserToken?: { token: string; userId: string }, snapshot?: AuthStoreSnapshot) {\n if (snapshot) this.loadSnapshot(snapshot);\n if (seedUserToken) this.addUserToken(seedUserToken.token, seedUserToken.userId);\n }\n\n addUserToken(token: string, userId: string): void {\n const existing = findSecretRecord(this.userTokens, token);\n if (existing?.userId === userId && !existing.revoked) return;\n const key = deriveSecretHash(token);\n this.userTokens.set(key.secretHash, { ...key, userId });\n this.afterMutation();\n }\n\n authenticateUser(token: string | undefined): string | null {\n if (!token) return null;\n const record = findSecretRecord(this.userTokens, token);\n if (!record || record.revoked) return null;\n return record.userId;\n }\n\n authenticateDevice(token: string | undefined): DeviceTokenRecord | null {\n if (!token) return null;\n const record = findSecretRecord(this.deviceTokens, token);\n if (!record || record.revoked) return null;\n return record;\n }\n\n createPairingCode(userId: string, ttlMs = 10 * 60 * 1000): { code: string; expiresAt: number } {\n const code = randomBytes(4).toString(\"hex\").toUpperCase();\n const expiresAt = Date.now() + ttlMs;\n const key = deriveSecretHash(code);\n this.pairingCodes.set(key.secretHash, { ...key, userId, expiresAt });\n this.afterMutation();\n return { code, expiresAt };\n }\n\n exchangePairingCode(code: string, runtimeName?: string): DeviceTokenIssue {\n const normalized = code.trim().toUpperCase();\n const codeEntry = findSecretEntry(this.pairingCodes, normalized);\n if (!codeEntry || codeEntry[1].expiresAt < Date.now()) {\n if (codeEntry) this.pairingCodes.delete(codeEntry[0]);\n this.afterMutation();\n throw new Error(\"Pairing code is invalid or expired\");\n }\n const [codeHash, record] = codeEntry;\n this.pairingCodes.delete(codeHash);\n const runtimeId = `rt_${randomUUID()}`;\n const deviceToken = `gsd_dev_${randomBytes(32).toString(\"hex\")}`;\n const key = deriveSecretHash(deviceToken);\n this.deviceTokens.set(key.secretHash, { ...key, userId: record.userId, runtimeId, runtimeName });\n this.afterMutation();\n return { userId: record.userId, runtimeId, deviceToken };\n }\n\n revokeDeviceToken(deviceToken: string): boolean {\n const record = findSecretRecord(this.deviceTokens, deviceToken);\n if (!record) return false;\n record.revoked = true;\n this.afterMutation();\n return true;\n }\n\n snapshot(): AuthStoreSnapshot {\n return {\n version: 1,\n userTokens: Array.from(this.userTokens.values()),\n deviceTokens: Array.from(this.deviceTokens.values()),\n pairingCodes: Array.from(this.pairingCodes.values()),\n };\n }\n\n protected afterMutation(): void {\n // Extension point for persistent stores.\n }\n\n private loadSnapshot(snapshot: AuthStoreSnapshot): void {\n for (const record of snapshot.userTokens ?? []) {\n this.userTokens.set(record.secretHash, record);\n }\n for (const record of snapshot.deviceTokens ?? []) {\n this.deviceTokens.set(record.secretHash, record);\n }\n for (const record of snapshot.pairingCodes ?? []) {\n if (record.expiresAt >= Date.now()) {\n this.pairingCodes.set(record.secretHash, record);\n }\n }\n }\n}\n\nexport class FileAuthStore extends InMemoryAuthStore {\n private readonly filePath: string;\n\n constructor(\n filePath: string,\n seedUserToken?: { token: string; userId: string },\n ) {\n super(undefined, readSnapshot(filePath));\n this.filePath = filePath;\n if (seedUserToken) this.addUserToken(seedUserToken.token, seedUserToken.userId);\n this.persist();\n }\n\n protected override afterMutation(): void {\n this.persist();\n }\n\n private persist(): void {\n mkdirSync(dirname(this.filePath), { recursive: true });\n const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;\n writeFileSync(tmp, `${JSON.stringify(this.snapshot(), null, 2)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n renameSync(tmp, this.filePath);\n }\n}\n\nexport function extractBearerToken(header: string | string[] | undefined): string | undefined {\n const value = Array.isArray(header) ? header[0] : header;\n if (!value || value.length <= \"Bearer \".length) return undefined;\n if (value.slice(0, \"Bearer\".length).toLowerCase() !== \"bearer\") return undefined;\n\n const firstSeparator = value.charCodeAt(\"Bearer\".length);\n if (firstSeparator !== 0x20 && firstSeparator !== 0x09) return undefined;\n\n let tokenStart = \"Bearer\".length + 1;\n while (tokenStart < value.length) {\n const char = value.charCodeAt(tokenStart);\n if (char !== 0x20 && char !== 0x09) break;\n tokenStart += 1;\n }\n\n return tokenStart < value.length ? value.slice(tokenStart) : undefined;\n}\n\nexport function deriveSecretHash(secret: string, secretSalt = randomBytes(16).toString(\"hex\")): SecretHashRecord {\n return {\n secretHash: scryptSync(secret, secretSalt, 32).toString(\"hex\"),\n secretSalt,\n };\n}\n\nfunction findSecretRecord<T extends SecretHashRecord>(records: Map<string, T>, secret: string): T | undefined {\n return findSecretEntry(records, secret)?.[1];\n}\n\nfunction findSecretEntry<T extends SecretHashRecord>(records: Map<string, T>, secret: string): [string, T] | undefined {\n for (const entry of records) {\n if (secretMatches(entry[1], secret)) return entry;\n }\n return undefined;\n}\n\nfunction secretMatches(record: SecretHashRecord, secret: string): boolean {\n const candidate = Buffer.from(deriveSecretHash(secret, record.secretSalt).secretHash, \"hex\");\n const expected = Buffer.from(record.secretHash, \"hex\");\n return candidate.length === expected.length && timingSafeEqual(candidate, expected);\n}\n\nfunction readSnapshot(filePath: string): AuthStoreSnapshot | undefined {\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf8\")) as Partial<AuthStoreSnapshot>;\n if (parsed.version !== 1) return undefined;\n return {\n version: 1,\n userTokens: Array.isArray(parsed.userTokens) ? parsed.userTokens as AuthStoreSnapshot[\"userTokens\"] : [],\n deviceTokens: Array.isArray(parsed.deviceTokens) ? parsed.deviceTokens as AuthStoreSnapshot[\"deviceTokens\"] : [],\n pairingCodes: Array.isArray(parsed.pairingCodes) ? parsed.pairingCodes as AuthStoreSnapshot[\"pairingCodes\"] : [],\n };\n } catch {\n return undefined;\n }\n}\n"]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { listenGateway } from "./server.js";
|
|
3
|
+
const portArg = process.argv.indexOf("--port");
|
|
4
|
+
const authStoreArg = process.argv.indexOf("--auth-store");
|
|
5
|
+
const port = portArg >= 0 ? Number(process.argv[portArg + 1]) : undefined;
|
|
6
|
+
const authStorePath = authStoreArg >= 0 ? process.argv[authStoreArg + 1] : undefined;
|
|
7
|
+
listenGateway({ port, authStorePath }).then(({ url }) => {
|
|
8
|
+
process.stderr.write(`[gsd-cloud-mcp-gateway] listening on ${url}\n`);
|
|
9
|
+
}).catch((err) => {
|
|
10
|
+
process.stderr.write(`[gsd-cloud-mcp-gateway] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;AAC1D,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1E,MAAM,aAAa,GAAG,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAErF,aAAa,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;IACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,wCAAwC,GAAG,IAAI,CAAC,CAAC;AACxE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7G,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { listenGateway } from \"./server.js\";\n\nconst portArg = process.argv.indexOf(\"--port\");\nconst authStoreArg = process.argv.indexOf(\"--auth-store\");\nconst port = portArg >= 0 ? Number(process.argv[portArg + 1]) : undefined;\nconst authStorePath = authStoreArg >= 0 ? process.argv[authStoreArg + 1] : undefined;\n\nlistenGateway({ port, authStorePath }).then(({ url }) => {\n process.stderr.write(`[gsd-cloud-mcp-gateway] listening on ${url}\\n`);\n}).catch((err) => {\n process.stderr.write(`[gsd-cloud-mcp-gateway] fatal: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(1);\n});\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FileAuthStore, InMemoryAuthStore, deriveSecretHash, extractBearerToken } from "./auth-store.js";
|
|
2
|
+
export { RuntimeRegistry } from "./runtime-registry.js";
|
|
3
|
+
export { CLOUD_GATEWAY_TOOL_NAMES, createGatewayMcpServer } from "./mcp.js";
|
|
4
|
+
export { createGatewayServer, listenGateway } from "./server.js";
|
|
5
|
+
export type { CloudProjectRecord, GatewayToRuntimeMessage, RuntimeProject, RuntimeToGatewayMessage, } from "./protocol.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,cAAc,EACd,uBAAuB,GACxB,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { FileAuthStore, InMemoryAuthStore, deriveSecretHash, extractBearerToken } from "./auth-store.js";
|
|
2
|
+
export { RuntimeRegistry } from "./runtime-registry.js";
|
|
3
|
+
export { CLOUD_GATEWAY_TOOL_NAMES, createGatewayMcpServer } from "./mcp.js";
|
|
4
|
+
export { createGatewayServer, listenGateway } from "./server.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export { FileAuthStore, InMemoryAuthStore, deriveSecretHash, extractBearerToken } from \"./auth-store.js\";\nexport { RuntimeRegistry } from \"./runtime-registry.js\";\nexport { CLOUD_GATEWAY_TOOL_NAMES, createGatewayMcpServer } from \"./mcp.js\";\nexport { createGatewayServer, listenGateway } from \"./server.js\";\nexport type {\n CloudProjectRecord,\n GatewayToRuntimeMessage,\n RuntimeProject,\n RuntimeToGatewayMessage,\n} from \"./protocol.js\";\n"]}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { RuntimeRegistry } from "./runtime-registry.js";
|
|
3
|
+
export declare const CLOUD_GATEWAY_TOOL_NAMES: readonly ["gsd_cloud_projects", "gsd_execute", "gsd_status", "gsd_result", "gsd_cancel", "gsd_query", "gsd_resolve_blocker", "gsd_progress", "gsd_roadmap", "gsd_history", "gsd_doctor", "gsd_captures", "gsd_knowledge", "gsd_graph", ...string[]];
|
|
4
|
+
export declare function createGatewayMcpServer(params: {
|
|
5
|
+
userId: string;
|
|
6
|
+
registry: RuntimeRegistry;
|
|
7
|
+
}): McpServer;
|
|
8
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAwB7D,eAAO,MAAM,wBAAwB,qPAI3B,CAAC;AAOX,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,eAAe,CAAC;CAC3B,GAAG,SAAS,CA2DZ"}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { WORKFLOW_TOOL_NAMES } from "@opengsd/mcp-server";
|
|
4
|
+
const SERVER_NAME = "gsd-cloud-gateway";
|
|
5
|
+
const SERVER_VERSION = "1.0.2";
|
|
6
|
+
const SESSION_TOOL_NAMES = [
|
|
7
|
+
"gsd_execute",
|
|
8
|
+
"gsd_status",
|
|
9
|
+
"gsd_result",
|
|
10
|
+
"gsd_cancel",
|
|
11
|
+
"gsd_query",
|
|
12
|
+
"gsd_resolve_blocker",
|
|
13
|
+
"gsd_progress",
|
|
14
|
+
"gsd_roadmap",
|
|
15
|
+
"gsd_history",
|
|
16
|
+
"gsd_doctor",
|
|
17
|
+
"gsd_captures",
|
|
18
|
+
"gsd_knowledge",
|
|
19
|
+
"gsd_graph",
|
|
20
|
+
];
|
|
21
|
+
const CLOUD_PROJECTS_TOOL = "gsd_cloud_projects";
|
|
22
|
+
export const CLOUD_GATEWAY_TOOL_NAMES = [
|
|
23
|
+
CLOUD_PROJECTS_TOOL,
|
|
24
|
+
...SESSION_TOOL_NAMES,
|
|
25
|
+
...WORKFLOW_TOOL_NAMES,
|
|
26
|
+
];
|
|
27
|
+
const passthroughSchema = z.object({
|
|
28
|
+
runtimeId: z.string().optional().describe("Connected Local GSD Runtime ID"),
|
|
29
|
+
projectAlias: z.string().optional().describe("Gateway project alias advertised by the Local GSD Runtime"),
|
|
30
|
+
}).passthrough();
|
|
31
|
+
export function createGatewayMcpServer(params) {
|
|
32
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
33
|
+
server.registerTool(CLOUD_PROJECTS_TOOL, {
|
|
34
|
+
description: "List projects currently advertised by connected Local GSD Runtimes.",
|
|
35
|
+
inputSchema: {},
|
|
36
|
+
}, async () => ({
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: JSON.stringify({ projects: params.registry.listProjects(params.userId) }, null, 2),
|
|
40
|
+
}],
|
|
41
|
+
}));
|
|
42
|
+
const seen = new Set([CLOUD_PROJECTS_TOOL]);
|
|
43
|
+
for (const toolName of [...SESSION_TOOL_NAMES, ...WORKFLOW_TOOL_NAMES]) {
|
|
44
|
+
if (seen.has(toolName))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(toolName);
|
|
47
|
+
server.registerTool(toolName, {
|
|
48
|
+
description: `Forward ${toolName} to a connected Local GSD Runtime through the Cloud MCP Gateway.`,
|
|
49
|
+
inputSchema: passthroughSchema,
|
|
50
|
+
}, async (args, extra) => {
|
|
51
|
+
try {
|
|
52
|
+
const result = await params.registry.callTool({
|
|
53
|
+
userId: params.userId,
|
|
54
|
+
toolName,
|
|
55
|
+
args: args,
|
|
56
|
+
signal: extra.signal,
|
|
57
|
+
});
|
|
58
|
+
if (isMcpToolResult(result))
|
|
59
|
+
return result;
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
64
|
+
}],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
isError: true,
|
|
70
|
+
content: [{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: err instanceof Error ? err.message : String(err),
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return server;
|
|
79
|
+
}
|
|
80
|
+
function isMcpToolResult(value) {
|
|
81
|
+
return !!value
|
|
82
|
+
&& typeof value === "object"
|
|
83
|
+
&& Array.isArray(value.content)
|
|
84
|
+
&& value.content.every((item) => item.type === "text" && typeof item.text === "string");
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { RuntimeRegistry } from \"./runtime-registry.js\";\nimport { WORKFLOW_TOOL_NAMES } from \"@opengsd/mcp-server\";\n\nconst SERVER_NAME = \"gsd-cloud-gateway\";\nconst SERVER_VERSION = \"1.0.2\";\n\nconst SESSION_TOOL_NAMES = [\n \"gsd_execute\",\n \"gsd_status\",\n \"gsd_result\",\n \"gsd_cancel\",\n \"gsd_query\",\n \"gsd_resolve_blocker\",\n \"gsd_progress\",\n \"gsd_roadmap\",\n \"gsd_history\",\n \"gsd_doctor\",\n \"gsd_captures\",\n \"gsd_knowledge\",\n \"gsd_graph\",\n] as const;\n\nconst CLOUD_PROJECTS_TOOL = \"gsd_cloud_projects\";\n\nexport const CLOUD_GATEWAY_TOOL_NAMES = [\n CLOUD_PROJECTS_TOOL,\n ...SESSION_TOOL_NAMES,\n ...WORKFLOW_TOOL_NAMES,\n] as const;\n\nconst passthroughSchema = z.object({\n runtimeId: z.string().optional().describe(\"Connected Local GSD Runtime ID\"),\n projectAlias: z.string().optional().describe(\"Gateway project alias advertised by the Local GSD Runtime\"),\n}).passthrough();\n\nexport function createGatewayMcpServer(params: {\n userId: string;\n registry: RuntimeRegistry;\n}): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: SERVER_VERSION },\n { capabilities: { tools: {} } },\n );\n\n server.registerTool(\n CLOUD_PROJECTS_TOOL,\n {\n description: \"List projects currently advertised by connected Local GSD Runtimes.\",\n inputSchema: {},\n },\n async () => ({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ projects: params.registry.listProjects(params.userId) }, null, 2),\n }],\n }),\n );\n\n const seen = new Set<string>([CLOUD_PROJECTS_TOOL]);\n for (const toolName of [...SESSION_TOOL_NAMES, ...WORKFLOW_TOOL_NAMES]) {\n if (seen.has(toolName)) continue;\n seen.add(toolName);\n server.registerTool(\n toolName,\n {\n description: `Forward ${toolName} to a connected Local GSD Runtime through the Cloud MCP Gateway.`,\n inputSchema: passthroughSchema,\n },\n async (args, extra) => {\n try {\n const result = await params.registry.callTool({\n userId: params.userId,\n toolName,\n args: args as Record<string, unknown>,\n signal: extra.signal,\n });\n if (isMcpToolResult(result)) return result as never;\n return {\n content: [{\n type: \"text\" as const,\n text: typeof result === \"string\" ? result : JSON.stringify(result, null, 2),\n }],\n };\n } catch (err) {\n return {\n isError: true,\n content: [{\n type: \"text\" as const,\n text: err instanceof Error ? err.message : String(err),\n }],\n };\n }\n },\n );\n }\n\n return server;\n}\n\nfunction isMcpToolResult(value: unknown): value is {\n content: Array<{ type: \"text\"; text: string }>;\n isError?: boolean;\n structuredContent?: unknown;\n} {\n return !!value\n && typeof value === \"object\"\n && Array.isArray((value as { content?: unknown }).content)\n && (value as { content: Array<{ type?: unknown; text?: unknown }> }).content.every(\n (item) => item.type === \"text\" && typeof item.text === \"string\",\n );\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface RuntimeProject {
|
|
2
|
+
alias: string;
|
|
3
|
+
path?: string;
|
|
4
|
+
repoIdentity: string;
|
|
5
|
+
remoteLabel?: string;
|
|
6
|
+
markers?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface RuntimeHelloMessage {
|
|
9
|
+
type: "hello";
|
|
10
|
+
runtimeId: string;
|
|
11
|
+
runtimeName?: string;
|
|
12
|
+
projects: RuntimeProject[];
|
|
13
|
+
}
|
|
14
|
+
export interface RuntimeProjectsMessage {
|
|
15
|
+
type: "projects";
|
|
16
|
+
runtimeId?: string;
|
|
17
|
+
projects: RuntimeProject[];
|
|
18
|
+
}
|
|
19
|
+
export interface RuntimeHeartbeatMessage {
|
|
20
|
+
type: "heartbeat";
|
|
21
|
+
runtimeId?: string;
|
|
22
|
+
at?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface RuntimeToolCallMessage {
|
|
25
|
+
type: "tool_call";
|
|
26
|
+
requestId: string;
|
|
27
|
+
toolName: string;
|
|
28
|
+
args: Record<string, unknown>;
|
|
29
|
+
projectAlias?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface RuntimeToolResultMessage {
|
|
32
|
+
type: "tool_result";
|
|
33
|
+
requestId: string;
|
|
34
|
+
result?: unknown;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface RuntimeCancelMessage {
|
|
38
|
+
type: "cancel";
|
|
39
|
+
requestId: string;
|
|
40
|
+
}
|
|
41
|
+
export type GatewayToRuntimeMessage = RuntimeToolCallMessage | RuntimeCancelMessage;
|
|
42
|
+
export type RuntimeToGatewayMessage = RuntimeHelloMessage | RuntimeProjectsMessage | RuntimeHeartbeatMessage | RuntimeToolResultMessage;
|
|
43
|
+
export interface CloudProjectRecord extends RuntimeProject {
|
|
44
|
+
runtimeId: string;
|
|
45
|
+
runtimeName?: string;
|
|
46
|
+
online: boolean;
|
|
47
|
+
lastSeenAt: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
50
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,GAAG,oBAAoB,CAAC;AAEpF,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,sBAAsB,GACtB,uBAAuB,GACvB,wBAAwB,CAAC;AAE7B,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AA8DA,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["export interface RuntimeProject {\n alias: string;\n path?: string;\n repoIdentity: string;\n remoteLabel?: string;\n markers?: string[];\n}\n\nexport interface RuntimeHelloMessage {\n type: \"hello\";\n runtimeId: string;\n runtimeName?: string;\n projects: RuntimeProject[];\n}\n\nexport interface RuntimeProjectsMessage {\n type: \"projects\";\n runtimeId?: string;\n projects: RuntimeProject[];\n}\n\nexport interface RuntimeHeartbeatMessage {\n type: \"heartbeat\";\n runtimeId?: string;\n at?: number;\n}\n\nexport interface RuntimeToolCallMessage {\n type: \"tool_call\";\n requestId: string;\n toolName: string;\n args: Record<string, unknown>;\n projectAlias?: string;\n}\n\nexport interface RuntimeToolResultMessage {\n type: \"tool_result\";\n requestId: string;\n result?: unknown;\n error?: string;\n}\n\nexport interface RuntimeCancelMessage {\n type: \"cancel\";\n requestId: string;\n}\n\nexport type GatewayToRuntimeMessage = RuntimeToolCallMessage | RuntimeCancelMessage;\n\nexport type RuntimeToGatewayMessage =\n | RuntimeHelloMessage\n | RuntimeProjectsMessage\n | RuntimeHeartbeatMessage\n | RuntimeToolResultMessage;\n\nexport interface CloudProjectRecord extends RuntimeProject {\n runtimeId: string;\n runtimeName?: string;\n online: boolean;\n lastSeenAt: number;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import type { CloudProjectRecord } from "./protocol.js";
|
|
3
|
+
export interface GatewayToolCall {
|
|
4
|
+
userId: string;
|
|
5
|
+
toolName: string;
|
|
6
|
+
args: Record<string, unknown>;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export declare class RuntimeRegistry {
|
|
10
|
+
private readonly runtimes;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
private readonly projectQueues;
|
|
13
|
+
attachRuntime(params: {
|
|
14
|
+
userId: string;
|
|
15
|
+
runtimeId: string;
|
|
16
|
+
runtimeName?: string;
|
|
17
|
+
socket: WebSocket;
|
|
18
|
+
}): void;
|
|
19
|
+
listProjects(userId: string): CloudProjectRecord[];
|
|
20
|
+
callTool(call: GatewayToolCall): Promise<unknown>;
|
|
21
|
+
private resolveTarget;
|
|
22
|
+
private forwardToolCall;
|
|
23
|
+
private handleMessage;
|
|
24
|
+
private send;
|
|
25
|
+
private failPendingForRuntime;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=runtime-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-registry.d.ts","sourceRoot":"","sources":["../src/runtime-registry.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,KAAK,EACV,kBAAkB,EAInB,MAAM,eAAe,CAAC;AAsBvB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuC;IAErE,aAAa,CAAC,MAAM,EAAE;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,SAAS,CAAC;KACnB,GAAG,IAAI;IA4BR,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAiB5C,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBvD,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,eAAe;IAuDvB,OAAO,CAAC,aAAa;IAiCrB,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,qBAAqB;CAS9B"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { isRecord } from "./protocol.js";
|
|
4
|
+
export class RuntimeRegistry {
|
|
5
|
+
runtimes = new Map();
|
|
6
|
+
pending = new Map();
|
|
7
|
+
projectQueues = new Map();
|
|
8
|
+
attachRuntime(params) {
|
|
9
|
+
const existing = this.runtimes.get(params.runtimeId);
|
|
10
|
+
if (existing && existing.socket !== params.socket) {
|
|
11
|
+
this.failPendingForRuntime(params.runtimeId, `Local GSD Runtime disconnected: ${params.runtimeId}`);
|
|
12
|
+
existing.socket.close(4000, "replaced");
|
|
13
|
+
}
|
|
14
|
+
const runtime = {
|
|
15
|
+
userId: params.userId,
|
|
16
|
+
runtimeId: params.runtimeId,
|
|
17
|
+
runtimeName: params.runtimeName,
|
|
18
|
+
socket: params.socket,
|
|
19
|
+
projects: [],
|
|
20
|
+
lastSeenAt: Date.now(),
|
|
21
|
+
};
|
|
22
|
+
this.runtimes.set(params.runtimeId, runtime);
|
|
23
|
+
params.socket.on("message", (data) => {
|
|
24
|
+
this.handleMessage(params.runtimeId, data.toString("utf8"));
|
|
25
|
+
});
|
|
26
|
+
params.socket.on("close", () => {
|
|
27
|
+
if (this.runtimes.get(params.runtimeId)?.socket === params.socket) {
|
|
28
|
+
this.runtimes.delete(params.runtimeId);
|
|
29
|
+
this.failPendingForRuntime(params.runtimeId, `Local GSD Runtime disconnected: ${params.runtimeId}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
listProjects(userId) {
|
|
34
|
+
const rows = [];
|
|
35
|
+
for (const runtime of this.runtimes.values()) {
|
|
36
|
+
if (runtime.userId !== userId)
|
|
37
|
+
continue;
|
|
38
|
+
for (const project of runtime.projects) {
|
|
39
|
+
rows.push({
|
|
40
|
+
...project,
|
|
41
|
+
runtimeId: runtime.runtimeId,
|
|
42
|
+
runtimeName: runtime.runtimeName,
|
|
43
|
+
online: true,
|
|
44
|
+
lastSeenAt: runtime.lastSeenAt,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return rows.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
49
|
+
}
|
|
50
|
+
async callTool(call) {
|
|
51
|
+
const target = this.resolveTarget(call.userId, call.args);
|
|
52
|
+
const projectKey = `${target.runtime.runtimeId}:${target.projectAlias ?? "__runtime__"}`;
|
|
53
|
+
const prior = this.projectQueues.get(projectKey) ?? Promise.resolve();
|
|
54
|
+
const run = prior.catch(() => undefined).then(() => this.forwardToolCall(target.runtime, {
|
|
55
|
+
...call,
|
|
56
|
+
args: target.args,
|
|
57
|
+
}, target.projectAlias));
|
|
58
|
+
const cleanup = run.catch(() => undefined).finally(() => {
|
|
59
|
+
if (this.projectQueues.get(projectKey) === run)
|
|
60
|
+
this.projectQueues.delete(projectKey);
|
|
61
|
+
if (this.projectQueues.get(projectKey) === cleanup)
|
|
62
|
+
this.projectQueues.delete(projectKey);
|
|
63
|
+
});
|
|
64
|
+
this.projectQueues.set(projectKey, cleanup);
|
|
65
|
+
return run;
|
|
66
|
+
}
|
|
67
|
+
resolveTarget(userId, rawArgs) {
|
|
68
|
+
const args = { ...rawArgs };
|
|
69
|
+
const runtimeId = typeof args.runtimeId === "string" ? args.runtimeId : undefined;
|
|
70
|
+
const projectAlias = typeof args.projectAlias === "string"
|
|
71
|
+
? args.projectAlias
|
|
72
|
+
: typeof args.projectDir === "string"
|
|
73
|
+
? args.projectDir
|
|
74
|
+
: undefined;
|
|
75
|
+
delete args.runtimeId;
|
|
76
|
+
delete args.projectAlias;
|
|
77
|
+
const candidates = Array.from(this.runtimes.values()).filter((runtime) => runtime.userId === userId);
|
|
78
|
+
if (runtimeId) {
|
|
79
|
+
const runtime = candidates.find((candidate) => candidate.runtimeId === runtimeId);
|
|
80
|
+
if (!runtime)
|
|
81
|
+
throw new Error(`Local GSD Runtime is offline: ${runtimeId}`);
|
|
82
|
+
return { runtime, projectAlias, args };
|
|
83
|
+
}
|
|
84
|
+
if (projectAlias) {
|
|
85
|
+
const matches = candidates.filter((runtime) => runtime.projects.some((project) => project.alias === projectAlias || project.path === projectAlias));
|
|
86
|
+
if (matches.length === 1)
|
|
87
|
+
return { runtime: matches[0], projectAlias, args };
|
|
88
|
+
if (matches.length > 1)
|
|
89
|
+
throw new Error(`Project alias is ambiguous: ${projectAlias}`);
|
|
90
|
+
}
|
|
91
|
+
if (candidates.length === 1)
|
|
92
|
+
return { runtime: candidates[0], projectAlias, args };
|
|
93
|
+
if (candidates.length === 0)
|
|
94
|
+
throw new Error("No Local GSD Runtime is connected");
|
|
95
|
+
throw new Error("runtimeId or projectAlias is required when multiple Local GSD Runtimes are connected");
|
|
96
|
+
}
|
|
97
|
+
forwardToolCall(runtime, call, projectAlias) {
|
|
98
|
+
const requestId = randomUUID();
|
|
99
|
+
const payload = {
|
|
100
|
+
type: "tool_call",
|
|
101
|
+
requestId,
|
|
102
|
+
toolName: call.toolName,
|
|
103
|
+
args: call.args,
|
|
104
|
+
projectAlias,
|
|
105
|
+
};
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
// Removes the abort listener so a long-lived signal reused across many
|
|
108
|
+
// calls doesn't retain a listener per resolved call.
|
|
109
|
+
const removeAbortListener = () => {
|
|
110
|
+
call.signal?.removeEventListener("abort", abort);
|
|
111
|
+
};
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
this.pending.delete(requestId);
|
|
114
|
+
removeAbortListener();
|
|
115
|
+
reject(new Error(`Timed out waiting for Local GSD Runtime response to ${call.toolName}`));
|
|
116
|
+
}, 10 * 60 * 1000);
|
|
117
|
+
const abort = () => {
|
|
118
|
+
this.send(runtime, { type: "cancel", requestId });
|
|
119
|
+
this.pending.delete(requestId);
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(new Error(`${call.toolName} cancelled by client`));
|
|
122
|
+
};
|
|
123
|
+
this.pending.set(requestId, {
|
|
124
|
+
runtimeId: runtime.runtimeId,
|
|
125
|
+
toolName: call.toolName,
|
|
126
|
+
resolve,
|
|
127
|
+
reject,
|
|
128
|
+
timer,
|
|
129
|
+
removeAbortListener,
|
|
130
|
+
});
|
|
131
|
+
if (call.signal?.aborted)
|
|
132
|
+
return abort();
|
|
133
|
+
call.signal?.addEventListener("abort", abort, { once: true });
|
|
134
|
+
try {
|
|
135
|
+
this.send(runtime, payload);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
removeAbortListener();
|
|
139
|
+
this.pending.delete(requestId);
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
handleMessage(runtimeId, text) {
|
|
146
|
+
let message;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(text);
|
|
149
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string")
|
|
150
|
+
return;
|
|
151
|
+
message = parsed;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const runtime = this.runtimes.get(runtimeId);
|
|
157
|
+
if (runtime)
|
|
158
|
+
runtime.lastSeenAt = Date.now();
|
|
159
|
+
if (message.type === "hello" && runtime) {
|
|
160
|
+
runtime.runtimeName = message.runtimeName ?? runtime.runtimeName;
|
|
161
|
+
runtime.projects = message.projects;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (message.type === "projects" && runtime) {
|
|
165
|
+
runtime.projects = message.projects;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (message.type === "tool_result") {
|
|
169
|
+
const pending = this.pending.get(message.requestId);
|
|
170
|
+
if (!pending)
|
|
171
|
+
return;
|
|
172
|
+
this.pending.delete(message.requestId);
|
|
173
|
+
clearTimeout(pending.timer);
|
|
174
|
+
pending.removeAbortListener();
|
|
175
|
+
if (message.error)
|
|
176
|
+
pending.reject(new Error(message.error));
|
|
177
|
+
else
|
|
178
|
+
pending.resolve(message.result);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
send(runtime, message) {
|
|
182
|
+
if (runtime.socket.readyState !== WebSocket.OPEN) {
|
|
183
|
+
throw new Error(`Local GSD Runtime is offline: ${runtime.runtimeId}`);
|
|
184
|
+
}
|
|
185
|
+
runtime.socket.send(JSON.stringify(message));
|
|
186
|
+
}
|
|
187
|
+
failPendingForRuntime(runtimeId, message) {
|
|
188
|
+
for (const [requestId, pending] of this.pending) {
|
|
189
|
+
if (pending.runtimeId !== runtimeId)
|
|
190
|
+
continue;
|
|
191
|
+
this.pending.delete(requestId);
|
|
192
|
+
clearTimeout(pending.timer);
|
|
193
|
+
pending.removeAbortListener();
|
|
194
|
+
pending.reject(new Error(`${message} while waiting for ${pending.toolName}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=runtime-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-registry.js","sourceRoot":"","sources":["../src/runtime-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport WebSocket from \"ws\";\nimport type {\n CloudProjectRecord,\n GatewayToRuntimeMessage,\n RuntimeProject,\n RuntimeToGatewayMessage,\n} from \"./protocol.js\";\nimport { isRecord } from \"./protocol.js\";\n\ninterface RuntimeConnection {\n runtimeId: string;\n userId: string;\n runtimeName?: string;\n socket: WebSocket;\n projects: RuntimeProject[];\n lastSeenAt: number;\n}\n\ninterface PendingCall {\n runtimeId: string;\n toolName: string;\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n /** Detaches the abort listener from the caller's signal on resolution. */\n removeAbortListener: () => void;\n}\n\nexport interface GatewayToolCall {\n userId: string;\n toolName: string;\n args: Record<string, unknown>;\n signal?: AbortSignal;\n}\n\nexport class RuntimeRegistry {\n private readonly runtimes = new Map<string, RuntimeConnection>();\n private readonly pending = new Map<string, PendingCall>();\n private readonly projectQueues = new Map<string, Promise<unknown>>();\n\n attachRuntime(params: {\n userId: string;\n runtimeId: string;\n runtimeName?: string;\n socket: WebSocket;\n }): void {\n const existing = this.runtimes.get(params.runtimeId);\n if (existing && existing.socket !== params.socket) {\n this.failPendingForRuntime(params.runtimeId, `Local GSD Runtime disconnected: ${params.runtimeId}`);\n existing.socket.close(4000, \"replaced\");\n }\n\n const runtime: RuntimeConnection = {\n userId: params.userId,\n runtimeId: params.runtimeId,\n runtimeName: params.runtimeName,\n socket: params.socket,\n projects: [],\n lastSeenAt: Date.now(),\n };\n this.runtimes.set(params.runtimeId, runtime);\n\n params.socket.on(\"message\", (data) => {\n this.handleMessage(params.runtimeId, data.toString(\"utf8\"));\n });\n params.socket.on(\"close\", () => {\n if (this.runtimes.get(params.runtimeId)?.socket === params.socket) {\n this.runtimes.delete(params.runtimeId);\n this.failPendingForRuntime(params.runtimeId, `Local GSD Runtime disconnected: ${params.runtimeId}`);\n }\n });\n }\n\n listProjects(userId: string): CloudProjectRecord[] {\n const rows: CloudProjectRecord[] = [];\n for (const runtime of this.runtimes.values()) {\n if (runtime.userId !== userId) continue;\n for (const project of runtime.projects) {\n rows.push({\n ...project,\n runtimeId: runtime.runtimeId,\n runtimeName: runtime.runtimeName,\n online: true,\n lastSeenAt: runtime.lastSeenAt,\n });\n }\n }\n return rows.sort((a, b) => a.alias.localeCompare(b.alias));\n }\n\n async callTool(call: GatewayToolCall): Promise<unknown> {\n const target = this.resolveTarget(call.userId, call.args);\n const projectKey = `${target.runtime.runtimeId}:${target.projectAlias ?? \"__runtime__\"}`;\n const prior = this.projectQueues.get(projectKey) ?? Promise.resolve();\n const run = prior.catch(() => undefined).then(() => this.forwardToolCall(target.runtime, {\n ...call,\n args: target.args,\n }, target.projectAlias));\n const cleanup = run.catch(() => undefined).finally(() => {\n if (this.projectQueues.get(projectKey) === run) this.projectQueues.delete(projectKey);\n if (this.projectQueues.get(projectKey) === cleanup) this.projectQueues.delete(projectKey);\n });\n this.projectQueues.set(projectKey, cleanup);\n return run;\n }\n\n private resolveTarget(userId: string, rawArgs: Record<string, unknown>): {\n runtime: RuntimeConnection;\n projectAlias?: string;\n args: Record<string, unknown>;\n } {\n const args = { ...rawArgs };\n const runtimeId = typeof args.runtimeId === \"string\" ? args.runtimeId : undefined;\n const projectAlias = typeof args.projectAlias === \"string\"\n ? args.projectAlias\n : typeof args.projectDir === \"string\"\n ? args.projectDir\n : undefined;\n delete args.runtimeId;\n delete args.projectAlias;\n\n const candidates = Array.from(this.runtimes.values()).filter((runtime) => runtime.userId === userId);\n if (runtimeId) {\n const runtime = candidates.find((candidate) => candidate.runtimeId === runtimeId);\n if (!runtime) throw new Error(`Local GSD Runtime is offline: ${runtimeId}`);\n return { runtime, projectAlias, args };\n }\n\n if (projectAlias) {\n const matches = candidates.filter((runtime) =>\n runtime.projects.some((project) => project.alias === projectAlias || project.path === projectAlias),\n );\n if (matches.length === 1) return { runtime: matches[0]!, projectAlias, args };\n if (matches.length > 1) throw new Error(`Project alias is ambiguous: ${projectAlias}`);\n }\n\n if (candidates.length === 1) return { runtime: candidates[0]!, projectAlias, args };\n if (candidates.length === 0) throw new Error(\"No Local GSD Runtime is connected\");\n throw new Error(\"runtimeId or projectAlias is required when multiple Local GSD Runtimes are connected\");\n }\n\n private forwardToolCall(\n runtime: RuntimeConnection,\n call: GatewayToolCall,\n projectAlias?: string,\n ): Promise<unknown> {\n const requestId = randomUUID();\n const payload: GatewayToRuntimeMessage = {\n type: \"tool_call\",\n requestId,\n toolName: call.toolName,\n args: call.args,\n projectAlias,\n };\n\n return new Promise((resolve, reject) => {\n // Removes the abort listener so a long-lived signal reused across many\n // calls doesn't retain a listener per resolved call.\n const removeAbortListener = () => {\n call.signal?.removeEventListener(\"abort\", abort);\n };\n const timer = setTimeout(() => {\n this.pending.delete(requestId);\n removeAbortListener();\n reject(new Error(`Timed out waiting for Local GSD Runtime response to ${call.toolName}`));\n }, 10 * 60 * 1000);\n\n const abort = () => {\n this.send(runtime, { type: \"cancel\", requestId });\n this.pending.delete(requestId);\n clearTimeout(timer);\n reject(new Error(`${call.toolName} cancelled by client`));\n };\n this.pending.set(requestId, {\n runtimeId: runtime.runtimeId,\n toolName: call.toolName,\n resolve,\n reject,\n timer,\n removeAbortListener,\n });\n\n if (call.signal?.aborted) return abort();\n call.signal?.addEventListener(\"abort\", abort, { once: true });\n\n try {\n this.send(runtime, payload);\n } catch (err) {\n removeAbortListener();\n this.pending.delete(requestId);\n clearTimeout(timer);\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n\n private handleMessage(runtimeId: string, text: string): void {\n let message: RuntimeToGatewayMessage;\n try {\n const parsed = JSON.parse(text) as unknown;\n if (!isRecord(parsed) || typeof parsed.type !== \"string\") return;\n message = parsed as unknown as RuntimeToGatewayMessage;\n } catch {\n return;\n }\n\n const runtime = this.runtimes.get(runtimeId);\n if (runtime) runtime.lastSeenAt = Date.now();\n\n if (message.type === \"hello\" && runtime) {\n runtime.runtimeName = message.runtimeName ?? runtime.runtimeName;\n runtime.projects = message.projects;\n return;\n }\n if (message.type === \"projects\" && runtime) {\n runtime.projects = message.projects;\n return;\n }\n if (message.type === \"tool_result\") {\n const pending = this.pending.get(message.requestId);\n if (!pending) return;\n this.pending.delete(message.requestId);\n clearTimeout(pending.timer);\n pending.removeAbortListener();\n if (message.error) pending.reject(new Error(message.error));\n else pending.resolve(message.result);\n }\n }\n\n private send(runtime: RuntimeConnection, message: GatewayToRuntimeMessage): void {\n if (runtime.socket.readyState !== WebSocket.OPEN) {\n throw new Error(`Local GSD Runtime is offline: ${runtime.runtimeId}`);\n }\n runtime.socket.send(JSON.stringify(message));\n }\n\n private failPendingForRuntime(runtimeId: string, message: string): void {\n for (const [requestId, pending] of this.pending) {\n if (pending.runtimeId !== runtimeId) continue;\n this.pending.delete(requestId);\n clearTimeout(pending.timer);\n pending.removeAbortListener();\n pending.reject(new Error(`${message} while waiting for ${pending.toolName}`));\n }\n }\n}\n"]}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import { InMemoryAuthStore } from "./auth-store.js";
|
|
3
|
+
import { RuntimeRegistry } from "./runtime-registry.js";
|
|
4
|
+
export interface GatewayServerOptions {
|
|
5
|
+
port?: number;
|
|
6
|
+
host?: string;
|
|
7
|
+
userToken?: string;
|
|
8
|
+
userId?: string;
|
|
9
|
+
authStorePath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function createGatewayServer(options?: GatewayServerOptions): {
|
|
12
|
+
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
13
|
+
auth: InMemoryAuthStore;
|
|
14
|
+
registry: RuntimeRegistry;
|
|
15
|
+
};
|
|
16
|
+
export declare function listenGateway(options?: GatewayServerOptions): Promise<{
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
url: string;
|
|
19
|
+
}>;
|
|
20
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAKpF,OAAO,EAAqC,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB;;;;EAuFrE;AAED,wBAAsB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC;IAC/E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CASD"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { createGatewayMcpServer } from "./mcp.js";
|
|
6
|
+
import { extractBearerToken, FileAuthStore, InMemoryAuthStore } from "./auth-store.js";
|
|
7
|
+
import { RuntimeRegistry } from "./runtime-registry.js";
|
|
8
|
+
const MAX_JSON_BODY_BYTES = 1024 * 1024;
|
|
9
|
+
export function createGatewayServer(options = {}) {
|
|
10
|
+
const userId = options.userId ?? "local-user";
|
|
11
|
+
const userToken = options.userToken ?? process.env.GSD_CLOUD_USER_TOKEN;
|
|
12
|
+
if (!userToken) {
|
|
13
|
+
throw new Error("GSD_CLOUD_USER_TOKEN is required");
|
|
14
|
+
}
|
|
15
|
+
const authStorePath = options.authStorePath ?? process.env.GSD_CLOUD_AUTH_STORE_PATH;
|
|
16
|
+
const auth = authStorePath
|
|
17
|
+
? new FileAuthStore(authStorePath, { token: userToken, userId })
|
|
18
|
+
: new InMemoryAuthStore({ token: userToken, userId });
|
|
19
|
+
const registry = new RuntimeRegistry();
|
|
20
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
21
|
+
const server = createServer(async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
if (req.method === "GET" && req.url === "/healthz") {
|
|
24
|
+
return sendJson(res, 200, { ok: true });
|
|
25
|
+
}
|
|
26
|
+
if (req.method === "POST" && req.url === "/pairing-codes") {
|
|
27
|
+
const authedUser = requireUser(req, auth);
|
|
28
|
+
if (!authedUser)
|
|
29
|
+
return sendJson(res, 401, { error: "Unauthorized" });
|
|
30
|
+
return sendJson(res, 200, auth.createPairingCode(authedUser));
|
|
31
|
+
}
|
|
32
|
+
if (req.method === "POST" && req.url === "/pairing/exchange") {
|
|
33
|
+
const body = await readJson(req);
|
|
34
|
+
const code = typeof body.code === "string" ? body.code : "";
|
|
35
|
+
const runtimeName = typeof body.runtimeName === "string" ? body.runtimeName : undefined;
|
|
36
|
+
try {
|
|
37
|
+
return sendJson(res, 200, auth.exchangePairingCode(code, runtimeName));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return sendJson(res, 400, { error: "Pairing code is invalid or expired" });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (req.url?.startsWith("/mcp")) {
|
|
44
|
+
const authedUser = requireUser(req, auth);
|
|
45
|
+
if (!authedUser)
|
|
46
|
+
return sendJson(res, 401, { error: "Unauthorized" });
|
|
47
|
+
const body = req.method === "POST" ? await readJson(req) : undefined;
|
|
48
|
+
const transport = new StreamableHTTPServerTransport({
|
|
49
|
+
sessionIdGenerator: undefined,
|
|
50
|
+
});
|
|
51
|
+
const mcp = createGatewayMcpServer({ userId: authedUser, registry });
|
|
52
|
+
await mcp.connect(transport);
|
|
53
|
+
await transport.handleRequest(req, res, body);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
sendJson(res, 404, { error: "Not found" });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof BadRequestError) {
|
|
60
|
+
return sendJson(res, 400, { error: err.message });
|
|
61
|
+
}
|
|
62
|
+
sendJson(res, 500, { error: "Internal server error" });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
server.on("upgrade", (req, socket, head) => {
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
68
|
+
if (url.pathname !== "/runtime/connect") {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const deviceToken = extractBearerToken(req.headers.authorization);
|
|
73
|
+
const device = auth.authenticateDevice(deviceToken);
|
|
74
|
+
if (!device) {
|
|
75
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
76
|
+
socket.destroy();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
80
|
+
registry.attachRuntime({
|
|
81
|
+
userId: device.userId,
|
|
82
|
+
runtimeId: device.runtimeId,
|
|
83
|
+
runtimeName: device.runtimeName,
|
|
84
|
+
socket: ws,
|
|
85
|
+
});
|
|
86
|
+
ws.send(JSON.stringify({ type: "connected", requestId: randomUUID(), runtimeId: device.runtimeId }));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
socket.destroy();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return { server, auth, registry };
|
|
94
|
+
}
|
|
95
|
+
export async function listenGateway(options = {}) {
|
|
96
|
+
const { server } = createGatewayServer(options);
|
|
97
|
+
const port = options.port ?? Number(process.env.PORT ?? 8787);
|
|
98
|
+
const host = options.host ?? "0.0.0.0";
|
|
99
|
+
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
100
|
+
return {
|
|
101
|
+
url: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`,
|
|
102
|
+
close: () => new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve())),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function requireUser(req, auth) {
|
|
106
|
+
return auth.authenticateUser(extractBearerToken(req.headers.authorization));
|
|
107
|
+
}
|
|
108
|
+
async function readJson(req) {
|
|
109
|
+
const chunks = [];
|
|
110
|
+
let totalBytes = 0;
|
|
111
|
+
for await (const chunk of req) {
|
|
112
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
113
|
+
totalBytes += buffer.byteLength;
|
|
114
|
+
if (totalBytes > MAX_JSON_BODY_BYTES) {
|
|
115
|
+
throw new BadRequestError("Request body too large");
|
|
116
|
+
}
|
|
117
|
+
chunks.push(buffer);
|
|
118
|
+
}
|
|
119
|
+
if (chunks.length === 0)
|
|
120
|
+
return {};
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
123
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
124
|
+
? parsed
|
|
125
|
+
: {};
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new BadRequestError("Invalid JSON request body");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function sendJson(res, status, body) {
|
|
132
|
+
if (res.headersSent)
|
|
133
|
+
return;
|
|
134
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
135
|
+
res.end(JSON.stringify(body));
|
|
136
|
+
}
|
|
137
|
+
class BadRequestError extends Error {
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { WebSocketServer } from \"ws\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createGatewayMcpServer } from \"./mcp.js\";\nimport { extractBearerToken, FileAuthStore, InMemoryAuthStore } from \"./auth-store.js\";\nimport { RuntimeRegistry } from \"./runtime-registry.js\";\n\nconst MAX_JSON_BODY_BYTES = 1024 * 1024;\n\nexport interface GatewayServerOptions {\n port?: number;\n host?: string;\n userToken?: string;\n userId?: string;\n authStorePath?: string;\n}\n\nexport function createGatewayServer(options: GatewayServerOptions = {}) {\n const userId = options.userId ?? \"local-user\";\n const userToken = options.userToken ?? process.env.GSD_CLOUD_USER_TOKEN;\n if (!userToken) {\n throw new Error(\"GSD_CLOUD_USER_TOKEN is required\");\n }\n const authStorePath = options.authStorePath ?? process.env.GSD_CLOUD_AUTH_STORE_PATH;\n const auth = authStorePath\n ? new FileAuthStore(authStorePath, { token: userToken, userId })\n : new InMemoryAuthStore({ token: userToken, userId });\n const registry = new RuntimeRegistry();\n const wss = new WebSocketServer({ noServer: true });\n\n const server = createServer(async (req, res) => {\n try {\n if (req.method === \"GET\" && req.url === \"/healthz\") {\n return sendJson(res, 200, { ok: true });\n }\n\n if (req.method === \"POST\" && req.url === \"/pairing-codes\") {\n const authedUser = requireUser(req, auth);\n if (!authedUser) return sendJson(res, 401, { error: \"Unauthorized\" });\n return sendJson(res, 200, auth.createPairingCode(authedUser));\n }\n\n if (req.method === \"POST\" && req.url === \"/pairing/exchange\") {\n const body = await readJson(req);\n const code = typeof body.code === \"string\" ? body.code : \"\";\n const runtimeName = typeof body.runtimeName === \"string\" ? body.runtimeName : undefined;\n try {\n return sendJson(res, 200, auth.exchangePairingCode(code, runtimeName));\n } catch {\n return sendJson(res, 400, { error: \"Pairing code is invalid or expired\" });\n }\n }\n\n if (req.url?.startsWith(\"/mcp\")) {\n const authedUser = requireUser(req, auth);\n if (!authedUser) return sendJson(res, 401, { error: \"Unauthorized\" });\n const body = req.method === \"POST\" ? await readJson(req) : undefined;\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n });\n const mcp = createGatewayMcpServer({ userId: authedUser, registry });\n await mcp.connect(transport);\n await transport.handleRequest(req, res, body);\n return;\n }\n\n sendJson(res, 404, { error: \"Not found\" });\n } catch (err) {\n if (err instanceof BadRequestError) {\n return sendJson(res, 400, { error: err.message });\n }\n sendJson(res, 500, { error: \"Internal server error\" });\n }\n });\n\n server.on(\"upgrade\", (req, socket, head) => {\n try {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (url.pathname !== \"/runtime/connect\") {\n socket.destroy();\n return;\n }\n const deviceToken = extractBearerToken(req.headers.authorization);\n const device = auth.authenticateDevice(deviceToken);\n if (!device) {\n socket.write(\"HTTP/1.1 401 Unauthorized\\r\\n\\r\\n\");\n socket.destroy();\n return;\n }\n wss.handleUpgrade(req, socket, head, (ws) => {\n registry.attachRuntime({\n userId: device.userId,\n runtimeId: device.runtimeId,\n runtimeName: device.runtimeName,\n socket: ws,\n });\n ws.send(JSON.stringify({ type: \"connected\", requestId: randomUUID(), runtimeId: device.runtimeId }));\n });\n } catch {\n socket.destroy();\n }\n });\n\n return { server, auth, registry };\n}\n\nexport async function listenGateway(options: GatewayServerOptions = {}): Promise<{\n close: () => Promise<void>;\n url: string;\n}> {\n const { server } = createGatewayServer(options);\n const port = options.port ?? Number(process.env.PORT ?? 8787);\n const host = options.host ?? \"0.0.0.0\";\n await new Promise<void>((resolve) => server.listen(port, host, resolve));\n return {\n url: `http://${host === \"0.0.0.0\" ? \"localhost\" : host}:${port}`,\n close: () => new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve())),\n };\n}\n\nfunction requireUser(req: IncomingMessage, auth: InMemoryAuthStore): string | null {\n return auth.authenticateUser(extractBearerToken(req.headers.authorization));\n}\n\nasync function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n totalBytes += buffer.byteLength;\n if (totalBytes > MAX_JSON_BODY_BYTES) {\n throw new BadRequestError(\"Request body too large\");\n }\n chunks.push(buffer);\n }\n if (chunks.length === 0) return {};\n try {\n const parsed = JSON.parse(Buffer.concat(chunks).toString(\"utf8\")) as unknown;\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : {};\n } catch {\n throw new BadRequestError(\"Invalid JSON request body\");\n }\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n if (res.headersSent) return;\n res.writeHead(status, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n\nclass BadRequestError extends Error {}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengsd/cloud-mcp-gateway",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Cloud MCP gateway for brokering remote MCP clients to local GSD runtimes",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/open-gsd/gsd-pi.git",
|
|
9
|
+
"directory": "packages/cloud-mcp-gateway"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"gsd-cloud-mcp-gateway": "./bin/gsd-cloud-mcp-gateway.js"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node ../../scripts/clean-package-dist.cjs && tsc",
|
|
28
|
+
"test": "pnpm run build && node --test dist/*.test.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
32
|
+
"@opengsd/mcp-server": "^1.2.0",
|
|
33
|
+
"ws": "^8.20.0",
|
|
34
|
+
"zod": "^4.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.12.0",
|
|
38
|
+
"@types/ws": "^8.5.13",
|
|
39
|
+
"typescript": "^5.4.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.0.0"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"README.md",
|
|
46
|
+
"bin",
|
|
47
|
+
"dist",
|
|
48
|
+
"!dist/**/*.test.*"
|
|
49
|
+
]
|
|
50
|
+
}
|