Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d64c8cb

Browse files
authored
Merge pull request #72 from Prescott-Data/nexus-mcp-adapter
Nexus mcp adapter
2 parents ac4a4fd + 2e417f8 commit d64c8cb

47 files changed

Lines changed: 4098 additions & 309 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,7 @@ tmp/
5656
*.tmp
5757

5858
# Documentation/Artifacts
59+
site/
5960
docs/site/
6061
node_modules/
62+
__pycache__/

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ The Nexus Framework is a provider-agnostic, secure integration layer for managin
66

77
The Nexus Framework requires two primary shared secrets to operate securely:
88

9-
1. **`ENCRYPTION_KEY`**: A 32-byte key used by the Broker to encrypt tokens at rest.
10-
2. **`STATE_KEY`**: A 32-byte key shared between the Broker and Gateway to sign and verify the OAuth `state` parameter.
9+
1. **`ENCRYPTION_KEY`**: A 32-byte key used by the Broker to encrypt tokens at rest.
10+
2. **`STATE_KEY`**: A 32-byte key shared between the Broker and Gateway to sign and verify the OAuth `state` parameter.
1111

1212
**Both services will refuse to start if these variables are missing or invalid.** In distributed deployments, the `STATE_KEY` **must** be identical across all Broker and Gateway instances, or OAuth callbacks will fail with "Invalid state" errors.
1313

@@ -32,18 +32,34 @@ docker-compose up -d --build
3232
- **Gateway**: http://localhost:8090
3333
- **Admin API Key**: Configured in `.env` (Default: `nexus-admin-key`)
3434

35+
## Client SDKs
36+
37+
Connect your application or MCP server to Nexus using the official SDK for your language:
38+
39+
| Language | Package | Install |
40+
|---|---|---|
41+
| **Go** | `nexus-sdk` | `go get github.com/Prescott-Data/nexus-framework/nexus-sdk@latest` |
42+
| **TypeScript** | `@dromos/nexus-sdk` | `npm install @dromos/nexus-sdk` |
43+
| **Python** | `nexus-sdk` | `pip install nexus-sdk` |
44+
45+
All SDKs provide full feature parity: connection management, token retrieval, MCP token injection, caching, retry logic, and structured errors.
46+
3547
## Documentation
3648

3749
- **[Architecture](docs/architecture.md)**: System overview, components, and data flow.
3850
- **[Deployment & Config](docs/deployment.md)**: How to configure, build, and deploy the services.
39-
- **[Agent Integration Guide](docs/guides/integrating-agents.md)**: How to build agents that consume connections (including the Go Bridge).
40-
- **[Provider Management Guide](docs/guides/managing-providers.md)**: How to register and configure identity providers (OAuth2, API Keys).
51+
- **[SDK Overview](docs/sdks/index.md)**: Choose your SDK and explore the feature matrix.
52+
- **[MCP Server Integration](docs/guides/mcp-integration.md)**: Build MCP servers with automatic token injection.
53+
- **[Agent Integration Guide](docs/guides/integrating-agents.md)**: How to build agents that consume connections.
54+
- **[Provider Management Guide](docs/guides/managing-providers.md)**: How to register and configure identity providers.
4155
- **[API Reference](docs/reference/api.md)**: Links to OpenAPI specifications.
4256
- **[Security Model](docs/reference/security-model.md)**: Security guardrails and hardening.
43-
- **[Technical Debt & Roadmap](docs/reference/tech-debt.md)**: Known issues and future plans.
4457

4558
## Quick Links
4659

4760
- **[Broker Service](nexus-broker/README.md)**: Backend service details.
4861
- **[Gateway Service](nexus-gateway/README.md)**: Frontend API service details.
49-
- **[Bridge Library](nexus-bridge/README.md)**: Go client library details.
62+
- **[Bridge Library](nexus-bridge/README.md)**: Go persistent connection client.
63+
- **[Go SDK](nexus-sdk/README.md)**: Go client SDK.
64+
- **[TypeScript SDK](nexus-sdk-ts/README.md)**: TypeScript/JavaScript client SDK.
65+
- **[Python SDK](nexus-sdk-python/README.md)**: Python client SDK.

docs/guides/integrating-agents.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,34 @@ payload, _ := client.GetToken(context.Background(), "your-connection-id")
122122
// Inspect the strategy to decide how to authenticate
123123
strategyType := payload.Strategy["type"]
124124
```
125-
See the [`nexus-sdk/README.md`](../../nexus-sdk/README.md) for more details.
125+
See the [Go SDK Reference](../sdks/go.md) for full documentation.
126+
127+
### Using the TypeScript SDK (server-side)
128+
129+
```typescript
130+
import { NexusClient } from '@dromos/nexus-sdk';
131+
132+
const client = new NexusClient({ gatewayUrl: 'https://<gateway-base-url>' });
133+
134+
const token = await client.getTokenByConnectionId('your-connection-id');
135+
console.log(token.accessToken);
136+
```
137+
See the [TypeScript SDK Reference](../sdks/typescript.md) for full documentation.
138+
139+
### Using the Python SDK (server-side)
140+
141+
```python
142+
from nexus_sdk import NexusClient, NexusClientOptions
143+
144+
client = NexusClient(NexusClientOptions(gateway_url='https://<gateway-base-url>'))
145+
146+
token = client.get_token_by_connection_id('your-connection-id')
147+
print(token.access_token)
148+
```
149+
See the [Python SDK Reference](../sdks/python.md) for full documentation.
150+
151+
---
152+
153+
## MCP Server Integration
154+
155+
For building MCP servers that automatically resolve and inject tokens, see the dedicated [MCP Server Integration Guide](mcp-integration.md).

docs/guides/mcp-integration.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# MCP Server Integration
2+
3+
This guide shows how to build a **Model Context Protocol (MCP) server** that uses the Nexus SDK to make authorized API calls on behalf of a workspace/tenant — in TypeScript, Go, and Python.
4+
5+
## What is MCP?
6+
7+
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard for exposing tools and data sources to AI agents. An **MCP server** exposes a set of tools (e.g., "list GitHub repos", "post a Slack message") that an AI agent can invoke. MCP servers typically run as child processes communicating over stdio.
8+
9+
When an MCP server needs to call a third-party API on behalf of a user, it needs an access token — and that token must be scoped to the right tenant. Nexus handles this automatically.
10+
11+
## How it Works
12+
13+
```
14+
AI Agent → MCP Client → MCP Server (your code) → Nexus SDK → Nexus Gateway → Upstream API
15+
16+
(resolves & caches token)
17+
```
18+
19+
1. The MCP server receives a tool invocation from the AI agent, e.g. `list_repos`.
20+
2. The tool handler calls the Nexus SDK's auth injector (`createFetcher` / `AuthenticatedHTTPClient` / `authenticated_fetch`).
21+
3. The SDK checks its token cache for the workspace+provider pair. On a cache miss, it calls `GET /v1/resolve` on the Nexus Gateway.
22+
4. The SDK injects the `Authorization: Bearer <token>` header and makes the upstream API call.
23+
5. The result is returned to the AI agent.
24+
25+
---
26+
27+
## TypeScript MCP Server
28+
29+
=== "Tool Handler"
30+
31+
```typescript
32+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
33+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
34+
import { NexusClient } from '@dromos/nexus-sdk';
35+
36+
const WORKSPACE_ID = process.env.WORKSPACE_ID!;
37+
const GATEWAY_URL = process.env.NEXUS_GATEWAY_URL!;
38+
39+
const nexus = new NexusClient({ gatewayUrl: GATEWAY_URL });
40+
const fetcher = nexus.createFetcher({ workspaceId: WORKSPACE_ID, provider: 'github' });
41+
42+
const server = new Server({ name: 'github-server', version: '1.0.0' }, {
43+
capabilities: { tools: {} },
44+
});
45+
46+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
47+
tools: [{
48+
name: 'list_repos',
49+
description: 'List GitHub repositories for the authenticated user',
50+
inputSchema: { type: 'object', properties: {}, required: [] },
51+
}],
52+
}));
53+
54+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
55+
if (req.params.name === 'list_repos') {
56+
const resp = await fetcher('https://api.github.com/user/repos?per_page=10&sort=updated');
57+
const repos = await resp.json() as any[];
58+
return {
59+
content: [{ type: 'text', text: repos.map(r => r.full_name).join('\n') }],
60+
};
61+
}
62+
throw new Error(`Unknown tool: ${req.params.name}`);
63+
});
64+
65+
const transport = new StdioServerTransport();
66+
await server.connect(transport);
67+
// All console.error() calls are safe — NexusClient already uses stderr only.
68+
console.error('[github-server] Running on stdio');
69+
```
70+
71+
!!! warning "stdout is sacred"
72+
In an MCP stdio server, stdout is the JSON-RPC channel. Any `console.log()` call will corrupt the protocol. Always use `console.error()` for diagnostics. The Nexus SDK already does this internally.
73+
74+
---
75+
76+
## Go MCP Server
77+
78+
Using [`mark3labs/mcp-go`](https://github.com/mark3labs/mcp-go):
79+
80+
```go
81+
package main
82+
83+
import (
84+
"context"
85+
"encoding/json"
86+
"io"
87+
"time"
88+
89+
"github.com/mark3labs/mcp-go/mcp"
90+
"github.com/mark3labs/mcp-go/server"
91+
oauthsdk "github.com/Prescott-Data/nexus-framework/nexus-sdk"
92+
)
93+
94+
func main() {
95+
nexus := oauthsdk.New(os.Getenv("NEXUS_GATEWAY_URL"))
96+
cache := oauthsdk.NewTokenCache(30 * time.Second)
97+
workspace := os.Getenv("WORKSPACE_ID")
98+
99+
// Authenticated HTTP client — token resolved and injected automatically
100+
gh := nexus.AuthenticatedHTTPClient(cache, workspace, "github")
101+
102+
s := server.NewMCPServer("github-server", "1.0.0")
103+
104+
s.AddTool(mcp.NewTool("list_repos",
105+
mcp.WithDescription("List GitHub repositories"),
106+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
107+
resp, err := gh.Get("https://api.github.com/user/repos?per_page=10&sort=updated")
108+
if err != nil {
109+
return mcp.NewToolResultError(err.Error()), nil
110+
}
111+
defer resp.Body.Close()
112+
113+
var repos []map[string]any
114+
json.NewDecoder(resp.Body).Decode(&repos)
115+
116+
names := make([]string, 0, len(repos))
117+
for _, r := range repos {
118+
names = append(names, r["full_name"].(string))
119+
}
120+
return mcp.NewToolResultText(strings.Join(names, "\n")), nil
121+
})
122+
123+
server.ServeStdio(s)
124+
}
125+
```
126+
127+
---
128+
129+
## Python MCP Server
130+
131+
Using the [`mcp`](https://github.com/modelcontextprotocol/python-sdk) Python package:
132+
133+
```python
134+
import asyncio
135+
import json
136+
import os
137+
138+
import mcp.server.stdio
139+
from mcp.server import Server
140+
from mcp.types import Tool, TextContent, CallToolResult
141+
142+
from nexus_sdk import NexusClient, NexusClientOptions, TokenCache
143+
144+
WORKSPACE_ID = os.environ["WORKSPACE_ID"]
145+
GATEWAY_URL = os.environ["NEXUS_GATEWAY_URL"]
146+
147+
nexus = NexusClient(NexusClientOptions(gateway_url=GATEWAY_URL))
148+
cache = TokenCache()
149+
150+
app = Server("github-server")
151+
152+
@app.list_tools()
153+
async def list_tools():
154+
return [Tool(
155+
name="list_repos",
156+
description="List GitHub repositories for the authenticated user",
157+
inputSchema={"type": "object", "properties": {}, "required": []},
158+
)]
159+
160+
@app.call_tool()
161+
async def call_tool(name: str, arguments: dict):
162+
if name == "list_repos":
163+
status, _, body = nexus.authenticated_fetch(
164+
cache, WORKSPACE_ID, "github",
165+
"https://api.github.com/user/repos?per_page=10&sort=updated",
166+
headers={"User-Agent": "my-mcp-server/1.0"},
167+
)
168+
repos = json.loads(body)
169+
names = "\n".join(r["full_name"] for r in repos)
170+
return [TextContent(type="text", text=names)]
171+
raise ValueError(f"Unknown tool: {name}")
172+
173+
async def main():
174+
async with mcp.server.stdio.stdio_server() as (r, w):
175+
await app.run(r, w, app.create_initialization_options())
176+
177+
if __name__ == "__main__":
178+
asyncio.run(main())
179+
```
180+
181+
---
182+
183+
## Common Pitfalls
184+
185+
| Issue | Cause | Fix |
186+
|---|---|---|
187+
| MCP client gets garbled responses | Using `console.log` / `print()` to stdout | Use `console.error` (TS) / `logging` to stderr (Python) |
188+
| 401 errors after a few minutes | Token TTL too long, caching stale tokens | SDK defaults to 5-min TTL when `expires_at` is missing |
189+
| Notion returns 401 with valid token | `token_type: "bearer"` (lowercase) sent as-is | SDK normalizes to `Bearer` per RFC 6750 |
190+
| `connection_not_found` error | No active OAuth connection for the workspace | Run the consent flow for that workspace+provider pair |
191+
| Cold start latency | First request resolves token synchronously | Pre-warm: call `get_cached_token` at server startup |
192+
193+
---
194+
195+
## Consent Flow
196+
197+
Before an MCP server can fetch tokens, a connection must be established. Initiate the OAuth consent flow with any of the SDKs:
198+
199+
=== "TypeScript"
200+
201+
```typescript
202+
const conn = await nexus.requestConnection({
203+
userId: 'workspace-123', providerName: 'github',
204+
scopes: ['repo'], returnUrl: 'https://your-app.com/callback',
205+
});
206+
console.log('Authorize here:', conn.authUrl);
207+
const status = await nexus.waitForActive(conn.connectionId);
208+
```
209+
210+
=== "Go"
211+
212+
```go
213+
conn, _ := nexus.RequestConnection(ctx, oauthsdk.RequestConnectionInput{
214+
UserID: "workspace-123", ProviderName: "github",
215+
Scopes: []string{"repo"}, ReturnURL: "https://your-app.com/callback",
216+
})
217+
log.Println("Authorize here:", conn.AuthURL)
218+
nexus.WaitForActive(ctx, conn.ConnectionID, 1500*time.Millisecond)
219+
```
220+
221+
=== "Python"
222+
223+
```python
224+
conn = nexus.request_connection(RequestConnectionInput(
225+
user_id='workspace-123', provider_name='github',
226+
scopes=['repo'], return_url='https://your-app.com/callback',
227+
))
228+
print('Authorize here:', conn.auth_url)
229+
nexus.wait_for_active(conn.connection_id)
230+
```

0 commit comments

Comments
 (0)