diff --git a/development/satellite/hierarchical-router.mdx b/development/satellite/hierarchical-router.mdx index 99efd2b..5267e6c 100644 --- a/development/satellite/hierarchical-router.mdx +++ b/development/satellite/hierarchical-router.mdx @@ -80,9 +80,9 @@ Instead of exposing all tools directly, the satellite exposes only 2 meta-tools: - 37.5% of 200k context consumed **After (Hierarchical):** -- 2 meta-tools × 175 tokens = 350 tokens -- 0.175% of 200k context consumed -- **99.5% reduction** +- 2 tools × 686 tokens = 1372 tokens consumed +- Result: 0.686% of context window used +- **Token Reduction: 98.3%** ## Meta-Tool Specifications @@ -527,8 +527,8 @@ Next time the client calls `discover_mcp_tools`, the new tools are automatically | Metric | Traditional | Hierarchical | Reduction | |--------|-------------|--------------|-----------| | Tools Exposed | 150 | 2 | 98.7% | -| Tokens Consumed | 75,000 | 350 | 99.5% | -| Context Available | 62.5% | 99.8% | +37.3% | +| Tokens Consumed | 75,000 | 1372 | 98.2% | +| Context Available | 62.5% | 99.3% | +36.8% | ### Search Performance @@ -541,8 +541,8 @@ Next time the client calls `discover_mcp_tools`, the new tools are automatically **Claude Code Example:** - Before: 82,000 tokens (41%) consumed by MCP tools -- After: 350 tokens (0.175%) consumed by meta-tools -- Result: **81,650 tokens freed for actual work** +- After: 1372 tokens (0.686%) consumed by meta-tools +- Result: **80,628 tokens freed for actual work** ## Implementation Status diff --git a/development/satellite/oauth-authentication.mdx b/development/satellite/oauth-authentication.mdx index ac1d0a6..951e0a8 100644 --- a/development/satellite/oauth-authentication.mdx +++ b/development/satellite/oauth-authentication.mdx @@ -268,6 +268,94 @@ const response = await fetch(`${backendUrl}/api/oauth2/introspect`, { - `active: false` - Token invalid, return authentication error - Team context includes: team_id, team_name, team_role, team_permissions +## Session Management and Security Model + +### MCP Sessions vs OAuth Authentication + +The satellite implements a two-layer security model that separates authentication from session management: + +**Authentication Layer (OAuth Bearer Token):** +- Primary security mechanism for all requests +- Validates user identity, team membership, and permissions +- Enforced by authentication middleware before session handling +- Team isolation enforced at this layer via token introspection + +**Session Layer (MCP Session ID):** +- Transport-level identifier for HTTP/SSE connection routing +- NOT a security credential - purely for protocol state management +- Can be safely reused because security comes from Bearer token +- Managed by StreamableHTTPServerTransport from MCP SDK + +### Session Resurrection After Satellite Restart + +When a satellite restarts (deployments, updates, crashes), MCP sessions are lost because they live in memory. The satellite implements transparent session resurrection to avoid forcing users to manually reconnect: + +**How Session Resurrection Works:** +1. Client sends request with old session ID (from before restart) +2. Satellite validates Bearer token FIRST (authentication layer) +3. If session ID is stale, satellite creates new Server + Transport with same session ID +4. Bootstrap transport with synthetic `initialize` request +5. Process actual client request normally +6. Client continues without reconnection + +**Implementation Details:** +```typescript +// Authentication happens FIRST (line 558 in mcp-server-wrapper.ts) +const authHeader = request.headers['authorization']; +const token = authHeader?.replace(/^Bearer\s+/i, ''); + +if (!token) { + return reply.status(401).send({ + jsonrpc: '2.0', + error: { code: -32001, message: 'Authentication required' }, + id: null + }); +} + +// Validate token via introspection BEFORE session handling +const introspectionResult = await this.tokenIntrospectionService.validateToken(token); + +if (!introspectionResult.valid) { + return reply.status(401).send({ + jsonrpc: '2.0', + error: { code: -32002, message: 'Invalid token' }, + id: null + }); +} + +// NOW handle session resurrection (lines 616-722) +const sessionId = request.headers['mcp-session-id']; +const existingTransport = this.transports.get(sessionId); + +if (!existingTransport && sessionId) { + // Create new Server + Transport with same session ID + server = new Server({ name: 'deploystack-satellite', version: '1.0.0' }); + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => sessionId, // Reuse old session ID + onsessioninitialized: (restoredSessionId) => { + this.transports.set(restoredSessionId, { transport, server }); + } + }); + + await server.connect(transport); + + // Bootstrap transport with synthetic initialize request + const syntheticInitRequest = { + jsonrpc: '2.0', + id: 0, + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'resurrected-session', version: '1.0.0' } + } + }; + + await transport.handleRequest(request.raw, mockRes, syntheticInitRequest); +} +``` + ## Team-Aware Tool Discovery ### Tool Filtering Implementation diff --git a/package-lock.json b/package-lock.json index 2e3cadc..1964354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.0-development", "devDependencies": { "@semantic-release/github": "^12.0.2", - "@types/node": "24.9.1", + "@types/node": "24.10.1", "markdownlint-cli": "^0.45.0", - "markdownlint-cli2": "^0.18.1", + "markdownlint-cli2": "^0.19.0", "typescript": "5.9.3" } }, @@ -578,9 +578,9 @@ } }, "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, "license": "MIT", "engines": { @@ -615,9 +615,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1786,20 +1786,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/execa/node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -2076,21 +2062,21 @@ } }, "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz", + "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==", "dev": true, "license": "MIT", "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", + "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", - "ignore": "^7.0.3", + "ignore": "^7.0.5", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2243,9 +2229,9 @@ } }, "node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2591,9 +2577,9 @@ "peer": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2860,18 +2846,18 @@ } }, "node_modules/markdownlint-cli2": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.18.1.tgz", - "integrity": "sha512-/4Osri9QFGCZOCTkfA8qJF+XGjKYERSHkXzxSyS1hd3ZERJGjvsUao2h4wdnvpHp6Tu2Jh/bPHM0FE9JJza6ng==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.19.0.tgz", + "integrity": "sha512-0+g7Fi/Y3qfvwfhJr77CpC/dEEoc4k7SvumlnL1tb68O+7fjKtIUG7aKzNUQIMXTVi8x63jcfXg4swz/ZYKyCw==", "dev": true, "license": "MIT", "dependencies": { - "globby": "14.1.0", - "js-yaml": "4.1.0", + "globby": "15.0.0", + "js-yaml": "4.1.1", "jsonc-parser": "3.3.1", "markdown-it": "14.1.0", - "markdownlint": "0.38.0", - "markdownlint-cli2-formatter-default": "0.0.5", + "markdownlint": "0.39.0", + "markdownlint-cli2-formatter-default": "0.0.6", "micromatch": "4.0.8" }, "bin": { @@ -2885,9 +2871,9 @@ } }, "node_modules/markdownlint-cli2-formatter-default": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.5.tgz", - "integrity": "sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", "dev": true, "license": "MIT", "funding": { @@ -2897,6 +2883,29 @@ "markdownlint-cli2": ">=0.0.4" } }, + "node_modules/markdownlint-cli2/node_modules/markdownlint": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.39.0.tgz", + "integrity": "sha512-Xt/oY7bAiHwukL1iru2np5LIkhwD19Y7frlsiDILK62v3jucXCD6JXlZlwMG12HZOR+roHIVuJZrfCkOhp6k3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, "node_modules/marked": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", diff --git a/package.json b/package.json index e10bd7b..ecfb202 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "dependencies": {}, "devDependencies": { "@semantic-release/github": "^12.0.2", - "@types/node": "24.9.1", + "@types/node": "24.10.1", "markdownlint-cli": "^0.45.0", - "markdownlint-cli2": "^0.18.1", + "markdownlint-cli2": "^0.19.0", "typescript": "5.9.3" } }