Nexus mcp adapter#72
Conversation
Providers like Notion return token_type as lowercase 'bearer' but their APIs reject the Authorization header unless it uses 'Bearer' (capitalized per RFC 6750). Case-insensitive comparison ensures all providers work regardless of how they return the token_type field. Verified: 3/3 MCP servers pass against live Azure gateway.
Unified TypeScript SDK that serves both standard apps and MCP servers.
Breaking changes:
- Renamed directory: nexus-mcp-adapter → nexus-sdk-ts
- Renamed package: nexus-mcp-adapter → @dromos/nexus-sdk
- NexusClient constructor now accepts optional retryPolicy and logger
- Console logging moved to structured NexusLogger interface
New features (Go SDK parity):
- requestConnection() — POST /v1/request-connection
- checkConnection() — GET /v1/check-connection/{id}
- getTokenByConnectionId() — GET /v1/token/{id}
- refreshConnection() — POST /v1/refresh/{id}
- waitForActive() — poll until active/failed with AbortSignal support
- Configurable RetryPolicy with exponential backoff + jitter
- NexusError class with structured error codes and status codes
- resolveToken() extracted as named public method
Retained MCP features:
- getToken() with TokenManager caching
- createFetcher() with automatic Authorization header injection
- RFC 6750 Bearer token normalization
- stderr-only logging (MCP stdio safe)
Verified: 3/3 MCP servers pass + Gateway API smoke test passes
against live Azure gateway.
… transport
Adds MCP-specific functionality to the Go SDK, achieving feature parity
with the TypeScript SDK (nexus-sdk-ts):
New files:
- resolve.go: ResolveToken (workspace+provider resolution via /v1/resolve),
GetCachedToken (cache-aware wrapper), AuthenticatedTransport (http.RoundTripper
that auto-injects Authorization headers), AuthenticatedHTTPClient (convenience)
- token_cache.go: Thread-safe in-memory TokenCache with configurable
safety buffer before expiry
- resolve_test.go: Tests for ResolveToken, TokenCache, GetCachedToken,
and AuthenticatedTransport
Usage:
client := oauthsdk.New("https://gateway.example.com")
httpClient := client.AuthenticatedHTTPClient(cache, "ws-001", "github")
resp, _ := httpClient.Get("https://api.github.com/user/repos")
// Authorization: Bearer <token> injected automatically
All 9 tests pass. go vet clean.
Validates ResolveToken, GetCachedToken, and AuthenticatedHTTPClient against the deployed Azure gateway with real GitHub/Google/Notion OAuth connections. All 5 tests pass.
Zero-dependency Python SDK using only stdlib (urllib.request).
Complete feature parity with Go and TypeScript SDKs.
Gateway API methods:
- request_connection() — POST /v1/request-connection
- check_connection() — GET /v1/check-connection/{id}
- get_token_by_connection_id() — GET /v1/token/{id}
- refresh_connection() — POST /v1/refresh/{id}
- wait_for_active() — poll until active/failed
MCP features:
- resolve_token() — GET /v1/resolve (workspace + provider)
- get_cached_token() — thread-safe TokenCache with TTL
- authenticated_fetch() — auto-injects Authorization headers
- Retry with exponential backoff + jitter
- NexusError with structured error codes
- stderr-only logging (MCP stdio safe)
Verified: 10/10 unit tests + 5/5 integration tests against live Azure gateway
There was a problem hiding this comment.
Pull request overview
This PR introduces MCP-focused token resolution + caching helpers across the Go, TypeScript, and Python Nexus SDKs (workspace+provider → token), and removes the older standalone nexus-mcp-adapter package in favor of the new unified TypeScript SDK.
Changes:
- Go SDK: add
/v1/resolvesupport, an in-memory token cache, and an authenticatedhttp.RoundTripper/http.Clienthelper. - TypeScript SDK: introduce a unified
NexusClientwith gateway API methods, token caching, andcreateFetcherfor MCP servers. - Python SDK: introduce a stdlib-only client with retrying gateway calls, token caching, and an authenticated fetch helper; add unit tests and smoke tests.
Reviewed changes
Copilot reviewed 22 out of 32 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
nexus-sdk/token_cache.go |
Adds a thread-safe in-memory token cache with expiry buffer support. |
nexus-sdk/resolve.go |
Adds /v1/resolve token resolution, cached resolution helper, and authenticated transport/client helpers. |
nexus-sdk/resolve_test.go |
Adds unit tests for resolve, cache behavior, and authenticated transport header injection. |
nexus-sdk/cmd/smoke-mcp/main.go |
Adds a live-gateway smoke test CLI for the Go SDK MCP flows. |
nexus-sdk-ts/tsconfig.json |
Introduces TypeScript compiler configuration for the new TS SDK package. |
nexus-sdk-ts/package.json |
Defines the new TS SDK package metadata, scripts, and dependencies. |
nexus-sdk-ts/package-lock.json |
Updates lockfile to match the new TS SDK package name/version. |
nexus-sdk-ts/src/types.ts |
Adds public TypeScript types for client config, gateway methods, token info, and errors. |
nexus-sdk-ts/src/TokenManager.ts |
Adds an LRU-based token cache manager with expiry-buffer eviction. |
nexus-sdk-ts/src/NexusClient.ts |
Implements the unified TS client (gateway APIs, retries, MCP resolve + fetcher). |
nexus-sdk-ts/src/index.ts |
Exposes the TS SDK public API surface. |
nexus-sdk-ts/tests/smoke-gateway-api.ts |
Adds a manual smoke test for gateway API methods in TS. |
nexus-sdk-ts/tests/runner.ts |
Adds an integration runner that spawns MCP servers and invokes tools via an MCP client. |
nexus-sdk-ts/tests/servers/github.ts |
Adds a GitHub MCP test server using createFetcher. |
nexus-sdk-ts/tests/servers/google.ts |
Adds a Google MCP test server using createFetcher. |
nexus-sdk-ts/tests/servers/notion.ts |
Adds a Notion MCP test server using createFetcher. |
nexus-sdk-ts/tests/servers/salesforce.ts |
Adds a Salesforce MCP test server using createFetcher. |
nexus-sdk-ts/tests/servers/slack.ts |
Adds a Slack MCP test server using createFetcher. |
nexus-sdk-ts/example/index.ts |
Adds an example MCP server showing how to use the TS client/fetcher. |
nexus-sdk-python/pyproject.toml |
Defines the Python SDK project metadata and dev extras. |
nexus-sdk-python/nexus_sdk/types.py |
Adds Python dataclasses for config, request/response models, and structured errors. |
nexus-sdk-python/nexus_sdk/token_cache.py |
Adds a thread-safe in-memory token cache with expiry buffer logic. |
nexus-sdk-python/nexus_sdk/client.py |
Implements the stdlib-only Python client: retries, gateway APIs, resolve/cache, authenticated fetch. |
nexus-sdk-python/nexus_sdk/__init__.py |
Exposes the Python SDK public API and version. |
nexus-sdk-python/tests/test_client.py |
Adds Python unit tests for cache, gateway calls, resolve, and header injection. |
nexus-sdk-python/tests/smoke_test.py |
Adds a live-gateway smoke test for the Python SDK MCP flows. |
nexus-sdk-python/tests/__init__.py |
Marks Python tests directory as a package. |
nexus-mcp-adapter/src/types.ts |
Removes the legacy adapter’s local type definitions. |
nexus-mcp-adapter/src/NexusClient.ts |
Removes the legacy adapter’s NexusClient implementation. |
nexus-mcp-adapter/src/index.ts |
Removes the legacy adapter’s public exports. |
nexus-mcp-adapter/package.json |
Removes the legacy adapter package metadata/deps. |
.gitignore |
Ignores Python __pycache__/ artifacts. |
Comments suppressed due to low confidence (3)
nexus-sdk/cmd/smoke-mcp/main.go:58
tok2.AccessToken[:10]can panic on short/empty tokens. Add a length check before slicing (or avoid slicing entirely in this smoke test output).
failed++
} else {
fmt.Printf("✅ token=%s... type=%s\n", tok2.AccessToken[:10], tok2.TokenType)
passed++
nexus-sdk/cmd/smoke-mcp/main.go:117
string(body2[:80])can panic when the response body is shorter than 80 bytes. Bound the slice length (and handle empty bodies) before slicing for error output.
passed++
} else {
fmt.Printf("❌ unexpected response: %s\n", string(body2[:80]))
failed++
}
nexus-sdk-python/nexus_sdk/client.py:20
- Unused import:
timezoneis imported fromdatetimebut not used. Remove it to keep the module lint-clean.
import urllib.request
from datetime import datetime, timezone
from typing import Any, Optional
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else { | ||
| fmt.Printf("✅ token=%s... type=%s\n", tok.AccessToken[:10], tok.TokenType) | ||
| passed++ |
| passed++ | ||
| } else { | ||
| fmt.Printf("❌ unexpected response: %s\n", string(body[:80])) | ||
| failed++ | ||
| } |
| signal.addEventListener( | ||
| 'abort', | ||
| () => { | ||
| clearTimeout(timer); | ||
| reject(new NexusError('aborted', 'waitForActive was aborted')); |
| """ | ||
| Nexus SDK for Python — unified client for Nexus OAuth Gateway. | ||
|
|
||
| Supports both synchronous and MCP server (async) workflows. |
| // Normalize token type per RFC 6750 | ||
| if (tokenInfo.tokenType.toLowerCase() === 'bearer') { | ||
| headers.set('Authorization', `Bearer ${tokenInfo.accessToken}`); | ||
| } else { | ||
| headers.set( |
| // Normalize token type per RFC 6750 | ||
| tokenType := token.TokenType | ||
| if strings.EqualFold(tokenType, "bearer") { | ||
| tokenType = "Bearer" | ||
| } |
| # Normalize token type per RFC 6750 | ||
| token_type = token.token_type | ||
| if token_type.lower() == "bearer": | ||
| token_type = "Bearer" | ||
| req_headers["Authorization"] = f"{token_type} {token.access_token}" |
…SDKs Per-SDK READMEs: - nexus-sdk/README.md: rewritten with MCP features, full API table, error handling, config options - nexus-sdk-ts/README.md: new, covers standard + MCP flows, ESM notes, type reference - nexus-sdk-python/README.md: new, covers standard + MCP flows, zero-dependency note, thread safety MkDocs site (docs/): - sdks/index.md: SDK overview with language selector, feature parity matrix, workflow explanation - sdks/go.md: full Go SDK method reference with examples - sdks/typescript.md: full TypeScript SDK reference, MCP stdio safety callout - sdks/python.md: full Python SDK reference, zero-dependency design note - guides/mcp-integration.md: new cross-cutting MCP guide with TS/Go/Python examples, pitfalls table - guides/integrating-agents.md: added TS/Python SDK examples + link to MCP guide; fixed broken relative link - services/sdk.md: redirects to new SDK section - mkdocs.yml: added SDKs multi-language nav section + MCP Integration Guide entry CHANGELOGs: - nexus-sdk-ts/CHANGELOG.md: v0.2.3 initial release - nexus-sdk-python/CHANGELOG.md: v0.2.3 initial release Root README: added Client SDKs table with install commands for all three SDKs Build: mkdocs build passes with 0 new warnings
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
| "main": "src/index.ts", | ||
| "types": "src/index.ts", |
| // 3. getTokenByConnectionId (using an existing active GitHub connection) | ||
| console.error('\n3. Testing getTokenByConnectionId (existing GitHub connection)...'); | ||
| try { | ||
| const token = await client.getTokenByConnectionId('d10f8c19-c468-445f-9fa8-f491e6f6071e'); | ||
| console.error(` ✅ Got token: ${token.accessToken.substring(0, 10)}...`); | ||
| console.error(` ✅ Token type: ${token.tokenType}`); | ||
| } catch (err: any) { | ||
| console.error(` ❌ ${err.message}`); | ||
| } | ||
|
|
||
| console.error('\n=== All Gateway API tests passed! ==='); | ||
| } |
| tok, err := client.ResolveToken(ctx, workspace, "github") | ||
| if err != nil { | ||
| fmt.Printf("❌ %v\n", err) | ||
| failed++ | ||
| } else { | ||
| fmt.Printf("✅ token=%s... type=%s\n", tok.AccessToken[:10], tok.TokenType) | ||
| passed++ |
| if login, ok := user["login"].(string); ok { | ||
| fmt.Printf("✅ user: %s\n", login) | ||
| passed++ | ||
| } else { | ||
| fmt.Printf("❌ unexpected response: %s\n", string(body[:80])) | ||
| failed++ | ||
| } |
| func main() { | ||
| gatewayURL := os.Getenv("NEXUS_GATEWAY_URL") | ||
| if gatewayURL == "" { | ||
| gatewayURL = "https://dromos-oauth-gateway.bravesea-3f5f7e75.eastus.azurecontainerapps.io" | ||
| } | ||
| workspace := "test-workspace-001" |
| ## Client SDKs | ||
|
|
||
| Connect your application or MCP server to Nexus using the official SDK for your language: | ||
|
|
||
| | Language | Package | Install | | ||
| |---|---|---| | ||
| | **Go** | `nexus-sdk` | `go get github.com/Prescott-Data/nexus-framework/nexus-sdk@latest` | | ||
| | **TypeScript** | `@dromos/nexus-sdk` | `npm install @dromos/nexus-sdk` | | ||
| | **Python** | `nexus-sdk` | `pip install nexus-sdk` | | ||
|
|
||
| All SDKs provide full feature parity: connection management, token retrieval, MCP token injection, caching, retry logic, and structured errors. |
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "lru-cache": "^11.3.6", | ||
| "zod": "^4.4.3" | ||
| }, | ||
| "devDependencies": { |
…ss all SDKs The auth injectors (createFetcher, RoundTrip, authenticated_fetch) previously hardcoded 'Authorization: Bearer <token>', which broke providers using non-OAuth strategies like API keys with custom headers (e.g., X-API-Key). Changes across all three SDKs: - Added headerName and valuePrefix to cached token types - resolveToken now extracts these from the broker's strategy.config: - strategy.config.header_name → which header to set - strategy.config.value_prefix → what to prepend (empty for raw keys) - Auth injectors now use these fields instead of hardcoding Authorization Examples of supported strategies: OAuth2: Authorization: Bearer <token> API key: X-API-Key: <token> (value_prefix is empty) Custom: X-Custom: prefix <token> Also fixed: Python SDK __init__.py docstring no longer claims async support All existing tests pass (Go: ok, Python: 10/10)
The package.json was pointing main/types at src/index.ts which Node cannot execute directly. Consumers importing this package would fail. Changes: - tsconfig.json: enabled rootDir=src, outDir=dist, added include/exclude - package.json: main → dist/index.js, types → dist/index.d.ts - Added exports map for proper ESM resolution - Added files whitelist (only dist/ is published) - Added prepublishOnly build guard - Fixed exactOptionalPropertyTypes errors (added | undefined) - Added .gitignore for dist/ Build: tsc compiles cleanly, dist/ contains JS + declarations + source maps
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 94 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (3)
nexus-sdk/cmd/smoke-mcp/main.go:47
- This prints the first 10 characters of live access tokens to stdout (
tok.AccessToken[:10]). Even partial tokens can be sensitive and may end up in CI logs or shared terminals. Please avoid logging any part of tokens (or only log a non-sensitive hash/fingerprint) in the smoke test output.
nexus-sdk-ts/tests/smoke-gateway-api.ts:38 - The smoke test hard-codes a connection ID and logs a substring of the access token. This is brittle (won’t work outside one environment) and risks leaking credentials into logs. Prefer taking the connection ID from an env var (or the
requestConnectionflow) and avoid logging any part of the token (use a non-sensitive fingerprint if needed).
nexus-sdk-python/tests/smoke_test.py:52 - The smoke test prints the first 10 characters of live access tokens (
tok.access_token[:10]). Even partial tokens can be sensitive and may leak via CI logs or terminal scrollback. Please avoid logging any portion of tokens (or log only a non-sensitive fingerprint) in this script.
print(" 1. resolve_token (github)... ", end="", flush=True)
try:
tok = client.resolve_token(WORKSPACE, "github")
print(f"✅ token={tok.access_token[:10]}... type={tok.token_type}")
passed += 1
except Exception as e:
print(f"❌ {e}")
failed += 1
# 2. resolve_token (Google)
print(" 2. resolve_token (google)... ", end="", flush=True)
try:
tok2 = client.resolve_token(WORKSPACE, "google")
print(f"✅ token={tok2.access_token[:10]}... type={tok2.token_type}")
passed += 1
| gatewayURL := os.Getenv("NEXUS_GATEWAY_URL") | ||
| if gatewayURL == "" { | ||
| gatewayURL = "https://dromos-oauth-gateway.bravesea-3f5f7e75.eastus.azurecontainerapps.io" | ||
| } |
| ```go | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
|
|
||
| "github.com/Prescott-Data/nexus-framework/nexus-sdk" | ||
| "context" | ||
| "time" | ||
| oauthsdk "github.com/Prescott-Data/nexus-framework/nexus-sdk" | ||
| ) | ||
|
|
||
| func main() { | ||
| ctx := context.Background() | ||
| client := oauthsdk.New("https://gateway.example.com") | ||
|
|
||
| // 1) Request a connection (this part of the flow is typically for user-interactive OAuth2) | ||
| // For service-to-service connections, you would likely have a pre-existing connection ID. | ||
| connectionID := "pre-existing-connection-id" | ||
|
|
||
| // 2) Get the credential payload for the connection | ||
| tr, err := client.GetToken(ctx, connectionID) | ||
| if err != nil { | ||
| log.Fatalf("Failed to get token: %v", err) | ||
| } | ||
|
|
||
| // 3) Inspect the response to determine how to authenticate | ||
|
|
||
| // The Strategy field tells you how to authenticate. | ||
| // It's a map that can be deserialized into a struct or inspected directly. | ||
| strategyType, _ := tr.Strategy["type"].(string) | ||
| fmt.Printf("Authentication Strategy: %s\n", strategyType) | ||
|
|
||
| // The Credentials field contains the secrets. | ||
| // It's a map that can be passed to an auth engine like the Bridge. | ||
| fmt.Printf("Credentials Map: %v\n", tr.Credentials) | ||
|
|
||
| // For backward compatibility with simple OAuth2 flows, AccessToken is still populated. | ||
| if strategyType == "oauth2" { | ||
| fmt.Printf("Access Token: %s\n", tr.AccessToken) | ||
| } | ||
| } | ||
| client := oauthsdk.New("https://nexus-gateway.example.com") | ||
|
|
||
| // 1. Initiate OAuth consent | ||
| conn, err := client.RequestConnection(ctx, oauthsdk.RequestConnectionInput{ | ||
| UserID: "user-123", |
| ## Notes | ||
|
|
||
| - The SDK **never logs token bodies**. | ||
| - All `RefreshConnection` calls go through the Gateway proxy — the Broker is never exposed to your application. | ||
| - Token types are normalized to `Bearer` (capitalized) per RFC 6750, regardless of how the provider returns them. |
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "lru-cache": "^11.3.6", | ||
| "zod": "^4.4.3" | ||
| }, | ||
| "devDependencies": { |
| ```typescript | ||
| import { Server } from '@modelcontextprotocol/sdk/server/index.js'; | ||
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||
| import { NexusClient } from '@dromos/nexus-sdk'; | ||
|
|
||
| const WORKSPACE_ID = process.env.WORKSPACE_ID!; | ||
| const GATEWAY_URL = process.env.NEXUS_GATEWAY_URL!; | ||
|
|
||
| const nexus = new NexusClient({ gatewayUrl: GATEWAY_URL }); | ||
| const fetcher = nexus.createFetcher({ workspaceId: WORKSPACE_ID, provider: 'github' }); | ||
|
|
||
| const server = new Server({ name: 'github-server', version: '1.0.0' }, { | ||
| capabilities: { tools: {} }, | ||
| }); | ||
|
|
||
| server.setRequestHandler(ListToolsRequestSchema, async () => ({ | ||
| tools: [{ | ||
| name: 'list_repos', | ||
| description: 'List GitHub repositories for the authenticated user', | ||
| inputSchema: { type: 'object', properties: {}, required: [] }, | ||
| }], | ||
| })); | ||
|
|
||
| server.setRequestHandler(CallToolRequestSchema, async (req) => { | ||
| if (req.params.name === 'list_repos') { |
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "io" | ||
| "time" | ||
|
|
||
| "github.com/mark3labs/mcp-go/mcp" | ||
| "github.com/mark3labs/mcp-go/server" | ||
| oauthsdk "github.com/Prescott-Data/nexus-framework/nexus-sdk" | ||
| ) |
| const GATEWAY_URL = process.env.NEXUS_GATEWAY_URL || 'https://dromos-oauth-gateway.bravesea-3f5f7e75.eastus.azurecontainerapps.io'; | ||
|
|
||
| async function main() { | ||
| const client = new NexusClient({ | ||
| gatewayUrl: GATEWAY_URL, | ||
| retryPolicy: { retries: 1 }, | ||
| }); |
| GATEWAY_URL = os.environ.get( | ||
| "NEXUS_GATEWAY_URL", | ||
| "https://dromos-oauth-gateway.bravesea-3f5f7e75.eastus.azurecontainerapps.io", | ||
| ) | ||
| WORKSPACE = "test-workspace-001" |
| ## Client SDKs | ||
|
|
||
| Connect your application or MCP server to Nexus using the official SDK for your language: | ||
|
|
||
| | Language | Package | Install | | ||
| |---|---|---| | ||
| | **Go** | `nexus-sdk` | `go get github.com/Prescott-Data/nexus-framework/nexus-sdk@latest` | | ||
| | **TypeScript** | `@dromos/nexus-sdk` | `npm install @dromos/nexus-sdk` | | ||
| | **Python** | `nexus-sdk` | `pip install nexus-sdk` | | ||
|
|
||
| All SDKs provide full feature parity: connection management, token retrieval, MCP token injection, caching, retry logic, and structured errors. |
Audit findings fixed: 1. zod moved from dependencies to devDependencies (only used in tests/example) 2. TS smoke test: require NEXUS_GATEWAY_URL env var (no live Azure default) 3. TS smoke test: hardcoded connection ID replaced with NEXUS_TEST_CONNECTION_ID env var 4. site/ build artifacts removed from git, added to .gitignore 5. nexus-sdk-ts/.gitignore: added node_modules/ Previously staged fixes also included: - Go smoke test: safePrefix helper for all slice operations - Go/Python smoke tests: require NEXUS_GATEWAY_URL env var - @modelcontextprotocol/sdk moved to devDependencies Verified: tsc ✅ | go build+test ✅ | python 10/10 ✅ | mkdocs build ✅
Pull Request
Description
Type of Change
Changes Made
How to Test
Migration / Breaking Changes
Checklist
gofmtapplied)