feat: add OAuth 2.0 authentication and README
- Implement RFC 8414 OAuth authorization server metadata - Add dynamic client registration (RFC 7591) - Add /authorize and /token endpoints - Create comprehensive README with setup instructions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
135
README.md
Normal file
135
README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Lupul Augmentat
|
||||
|
||||
MCP (Model Context Protocol) server with SSE transport for remote AI agent communication.
|
||||
|
||||
## Features
|
||||
|
||||
- **SSE Transport**: HTTP-based Server-Sent Events for remote MCP connections
|
||||
- **OAuth 2.0 Authentication**: RFC 8414 compliant for Claude Code compatibility
|
||||
- **NATS Messaging**: Internal pub/sub for module communication
|
||||
- **Agent Presence**: Track online agents and their capabilities
|
||||
- **Service Discovery**: List local services via systemctl
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone git@git.runningwolf.com:sebastian/Lupul-Augmentat.git
|
||||
cd Lupul-Augmentat
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```env
|
||||
# MCP Server
|
||||
MCP_HOST=127.0.0.1
|
||||
MCP_PORT=19017
|
||||
MCP_LOG_LEVEL=info
|
||||
|
||||
# NATS
|
||||
NATS_URL=nats://localhost:4222
|
||||
|
||||
# Security
|
||||
API_KEY=your-secure-api-key
|
||||
AUTH_ENABLED=true
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm run dev:sse
|
||||
```
|
||||
|
||||
### Production (systemd)
|
||||
```bash
|
||||
sudo systemctl start lupul-augmentat
|
||||
sudo systemctl enable lupul-augmentat
|
||||
```
|
||||
|
||||
## Connecting from Claude Code
|
||||
|
||||
Add to your MCP settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"lupul": {
|
||||
"type": "sse",
|
||||
"url": "https://your-server.com/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Claude Code will automatically handle OAuth authentication.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Agent Management
|
||||
- `register_agent` - Register yourself with name, role, and capabilities
|
||||
- `list_agents` - See all online agents
|
||||
- `list_services` - List services on this machine
|
||||
|
||||
### Messaging
|
||||
- `send_message` - Send message to another agent
|
||||
- `receive_messages` - Check your inbox
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /sse` | SSE connection (protected) |
|
||||
| `POST /message` | Send MCP messages (protected) |
|
||||
| `GET /health` | Health check |
|
||||
| `GET /.well-known/oauth-authorization-server` | OAuth metadata |
|
||||
| `GET /.well-known/oauth-protected-resource` | Protected resource metadata |
|
||||
| `POST /register` | Dynamic client registration |
|
||||
| `GET /authorize` | OAuth authorization |
|
||||
| `POST /token` | Token exchange |
|
||||
|
||||
## Nginx Configuration
|
||||
|
||||
For SSL termination with nginx:
|
||||
|
||||
```nginx
|
||||
location /sse {
|
||||
proxy_pass http://127.0.0.1:19017/sse;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 86400s;
|
||||
chunked_transfer_encoding off;
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Claude Code (Mac/Linux)
|
||||
│
|
||||
▼ HTTPS/SSE
|
||||
┌─────────────────┐
|
||||
│ nginx (SSL) │
|
||||
└────────┬────────┘
|
||||
│ HTTP
|
||||
┌────────▼────────┐
|
||||
│ SSE Server │
|
||||
│ (sse-server.ts)│
|
||||
└────────┬────────┘
|
||||
│ NATS
|
||||
┌────────▼────────┐
|
||||
│ Tool Modules │
|
||||
│ (messaging, │
|
||||
│ file ops...) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -67,26 +67,94 @@ async function startSSEServer() {
|
||||
res.send('healthy\n');
|
||||
});
|
||||
|
||||
// OAuth discovery endpoints - return empty/minimal responses
|
||||
// Claude Code checks these before connecting
|
||||
// OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
// This tells Claude Code how to authenticate
|
||||
const serverUrl = 'https://ultra.runningwolf.com';
|
||||
|
||||
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
|
||||
res.json({
|
||||
issuer: serverUrl,
|
||||
authorization_endpoint: `${serverUrl}/authorize`,
|
||||
token_endpoint: `${serverUrl}/token`,
|
||||
registration_endpoint: `${serverUrl}/register`,
|
||||
response_types_supported: ['code'],
|
||||
grant_types_supported: ['authorization_code'],
|
||||
code_challenge_methods_supported: ['S256'],
|
||||
token_endpoint_auth_methods_supported: ['none'],
|
||||
});
|
||||
});
|
||||
|
||||
// Protected Resource Metadata (RFC 9728)
|
||||
app.get('/.well-known/oauth-protected-resource', (_req, res) => {
|
||||
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
|
||||
res.json({
|
||||
resource: serverUrl,
|
||||
authorization_servers: [serverUrl],
|
||||
bearer_methods_supported: ['header'],
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/.well-known/oauth-protected-resource/*', (_req, res) => {
|
||||
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
|
||||
});
|
||||
|
||||
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
|
||||
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
|
||||
res.json({
|
||||
resource: serverUrl,
|
||||
authorization_servers: [serverUrl],
|
||||
bearer_methods_supported: ['header'],
|
||||
});
|
||||
});
|
||||
|
||||
// OpenID Connect discovery (optional, return 404)
|
||||
app.get('/.well-known/openid-configuration', (_req, res) => {
|
||||
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
|
||||
res.status(404).json({ error: 'not_supported' });
|
||||
});
|
||||
|
||||
app.post('/register', (_req, res) => {
|
||||
res.status(404).json({ error: 'not_found', error_description: 'OAuth not supported' });
|
||||
// Dynamic Client Registration (RFC 7591)
|
||||
// Auto-register any client that asks
|
||||
app.post('/register', (req, res) => {
|
||||
const clientId = `client_${Date.now()}`;
|
||||
res.status(201).json({
|
||||
client_id: clientId,
|
||||
client_secret: '', // Public client, no secret needed
|
||||
client_id_issued_at: Math.floor(Date.now() / 1000),
|
||||
grant_types: ['authorization_code'],
|
||||
response_types: ['code'],
|
||||
token_endpoint_auth_method: 'none',
|
||||
});
|
||||
});
|
||||
|
||||
// Authorization endpoint - return the API key as code
|
||||
app.get('/authorize', (req, res) => {
|
||||
const redirectUri = req.query.redirect_uri as string;
|
||||
const state = req.query.state as string;
|
||||
|
||||
if (!redirectUri) {
|
||||
res.status(400).json({ error: 'invalid_request', error_description: 'Missing redirect_uri' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the API key as the authorization code
|
||||
const code = API_KEY;
|
||||
const redirectUrl = new URL(redirectUri);
|
||||
redirectUrl.searchParams.set('code', code || 'no-api-key');
|
||||
if (state) redirectUrl.searchParams.set('state', state);
|
||||
|
||||
res.redirect(redirectUrl.toString());
|
||||
});
|
||||
|
||||
// Token endpoint - exchange code for access token
|
||||
app.post('/token', express.urlencoded({ extended: true }), (req, res) => {
|
||||
const code = req.body.code;
|
||||
|
||||
// Verify the code is our API key
|
||||
if (code !== API_KEY) {
|
||||
res.status(400).json({ error: 'invalid_grant', error_description: 'Invalid authorization code' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the API key as access token
|
||||
res.json({
|
||||
access_token: API_KEY,
|
||||
token_type: 'Bearer',
|
||||
expires_in: 86400 * 365, // 1 year
|
||||
});
|
||||
});
|
||||
|
||||
// SSE endpoint - establishes the SSE stream (protected)
|
||||
|
||||
Reference in New Issue
Block a user