Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf2c4d4c7 |
8
app.js
8
app.js
@@ -714,10 +714,6 @@ async function init() {
|
||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.copy(playerPos);
|
||||
|
||||
// Initialize avatar and LOD systems
|
||||
if (window.AvatarCustomization) window.AvatarCustomization.init(scene, camera);
|
||||
if (window.LODSystem) window.LODSystem.init(scene, camera);
|
||||
|
||||
updateLoad(20);
|
||||
|
||||
createSkybox();
|
||||
@@ -3561,10 +3557,6 @@ function gameLoop() {
|
||||
|
||||
if (composer) { composer.render(); } else { renderer.render(scene, camera); }
|
||||
|
||||
// Update avatar and LOD systems
|
||||
if (window.AvatarCustomization && playerPos) window.AvatarCustomization.update(playerPos);
|
||||
if (window.LODSystem && playerPos) window.LODSystem.update(playerPos);
|
||||
|
||||
updateAshStorm(delta, elapsed);
|
||||
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
|
||||
175
docs/hermes-mcp.md
Normal file
175
docs/hermes-mcp.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Hermes MCP Integration — Model Context Protocol
|
||||
|
||||
Issue #1121. Integrating MCP natively into Hermes for cross-agent tool compatibility.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
Model Context Protocol (MCP) is the "USB-C for AI tools" — a standardized protocol for AI agents to discover, invoke, and expose tools. Claude Desktop, Cursor, and a growing ecosystem speak it.
|
||||
|
||||
Hermes currently has a bespoke tool system (`tools/*.py`). Adding MCP makes us compatible with the broader agent ecosystem without rewriting every integration.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Hermes Agent │
|
||||
│ ┌───────────┐ ┌───────────────┐ │
|
||||
│ │ MCP Client│ │ MCP Server │ │
|
||||
│ │ (outbound)│ │ (inbound) │ │
|
||||
│ └─────┬─────┘ └───────┬───────┘ │
|
||||
│ │ │ │
|
||||
│ ┌─────┴─────┐ ┌───────┴───────┐ │
|
||||
│ │ External │ │ External │ │
|
||||
│ │ MCP │ │ MCP Clients │ │
|
||||
│ │ Servers │ │ (Claude, │ │
|
||||
│ │ (tools) │ │ Cursor, etc) │ │
|
||||
│ └───────────┘ └───────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Phase 1: MCP Client — Call External Servers
|
||||
|
||||
### Configuration
|
||||
|
||||
Hermes loads MCP servers from `~/.hermes/mcp_servers.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"desktop-control": {
|
||||
"command": "python3",
|
||||
"args": ["mcp_servers/desktop_control_server.py"]
|
||||
},
|
||||
"steam-info": {
|
||||
"command": "python3",
|
||||
"args": ["mcp_servers/steam_info_server.py"]
|
||||
},
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. On startup, `tools/mcp_tool.py` reads `mcp_servers.json`
|
||||
2. For each server, spawns the process and initializes MCP connection
|
||||
3. Discovers tools via MCP `tools/list` endpoint
|
||||
4. Registers discovered tools in the Hermes tool registry
|
||||
5. Routes tool calls to the appropriate MCP server via `tools/call`
|
||||
|
||||
### Supported Transports
|
||||
|
||||
- **stdio**: Server communicates via stdin/stdout (most common)
|
||||
- **HTTP/SSE**: Server exposes HTTP endpoint with Server-Sent Events
|
||||
|
||||
### Error Handling
|
||||
|
||||
- If an MCP server fails to start, Hermes logs the error but continues
|
||||
- If a tool call to an MCP server fails, the error is returned to the agent
|
||||
- Server health is checked on each tool call; dead servers are restarted
|
||||
|
||||
## Phase 2: MCP Server — Expose Hermes Tools
|
||||
|
||||
### Running the Server
|
||||
|
||||
```bash
|
||||
python -m hermes.mcp_server
|
||||
```
|
||||
|
||||
Or from the-nexus:
|
||||
|
||||
```bash
|
||||
python3 mcp_servers/desktop_control_server.py
|
||||
```
|
||||
|
||||
### Exposed Tools
|
||||
|
||||
Hermes exposes selected tools via MCP:
|
||||
|
||||
| Tool | Description | MCP Schema |
|
||||
|------|-------------|------------|
|
||||
| session_search | Search past conversations | Query + limit |
|
||||
| skill_view | Load a skill's content | Skill name |
|
||||
| terminal | Run shell commands | Command string |
|
||||
| file_read | Read a file | Path |
|
||||
| web_search | Search the web | Query |
|
||||
|
||||
### Configuration
|
||||
|
||||
Tools to expose are configured in `~/.hermes/mcp_server_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"expose_tools": ["session_search", "skill_view", "terminal", "file_read"],
|
||||
"require_auth": true,
|
||||
"auth_token": "${MCP_SERVER_TOKEN}"
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 3: Integration + Hardening
|
||||
|
||||
### Poka-Yoke (Error-Proofing)
|
||||
|
||||
1. **Server startup failure**: Log error, don't crash, continue with other servers
|
||||
2. **Tool discovery failure**: Skip that server's tools, log warning
|
||||
3. **Tool call timeout**: Return error to agent, don't hang
|
||||
4. **Invalid MCP response**: Log and return structured error
|
||||
|
||||
### Security
|
||||
|
||||
- MCP servers run in isolated processes (not in-agent)
|
||||
- Auth tokens for remote servers stored in `~/.hermes/.env`
|
||||
- Tool calls are logged for audit
|
||||
- Dangerous tools (terminal, file write) are NOT exposed via MCP server by default
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Test MCP client
|
||||
pytest tests/test_mcp.py -v -k client
|
||||
|
||||
# Test MCP server
|
||||
pytest tests/test_mcp.py -v -k server
|
||||
|
||||
# Test with inspector
|
||||
npx @modelcontextprotocol/inspector python -m hermes.mcp_server
|
||||
```
|
||||
|
||||
## Existing MCP Code
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `tools/mcp_tool.py` | MCP client tool implementation |
|
||||
| `tools/mcp_oauth.py` | OAuth support for remote MCP servers |
|
||||
| `mcp_config.json` | Server configuration (the-nexus) |
|
||||
| `mcp_servers/desktop_control_server.py` | Desktop control MCP server |
|
||||
| `mcp_servers/steam_info_server.py` | Steam info MCP server |
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install MCP SDK: `pip install mcp>=1.0.0`
|
||||
2. Configure servers: edit `~/.hermes/mcp_servers.json`
|
||||
3. Start Hermes: MCP servers are loaded automatically
|
||||
4. Verify: run `hermes tools list` to see MCP-discovered tools
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| MCP server won't start | Check command path, run manually to see error |
|
||||
| Tools not discovered | Check server responds to `tools/list` |
|
||||
| Tool call fails | Check server logs, verify auth tokens |
|
||||
| Hermes hangs on startup | MCP server timeout — increase or disable slow server |
|
||||
|
||||
## Sources
|
||||
|
||||
- MCP Specification: https://modelcontextprotocol.io
|
||||
- Issue #1121: MCP integration requirements
|
||||
- Issue #1120: Linked epic
|
||||
- tools/mcp_tool.py: Existing Hermes MCP implementation
|
||||
@@ -395,8 +395,6 @@
|
||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||
|
||||
<script src="./boot.js"></script>
|
||||
<script src="./avatar-customization.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script>
|
||||
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
|
||||
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }
|
||||
|
||||
186
lod-system.js
186
lod-system.js
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* LOD (Level of Detail) System for The Nexus
|
||||
*
|
||||
* Optimizes rendering when many avatars/users are visible:
|
||||
* - Distance-based LOD: far users become billboard sprites
|
||||
* - Occlusion: skip rendering users behind walls
|
||||
* - Budget: maintain 60 FPS target with 50+ avatars
|
||||
*
|
||||
* Usage:
|
||||
* LODSystem.init(scene, camera);
|
||||
* LODSystem.registerAvatar(avatarMesh, userId);
|
||||
* LODSystem.update(playerPos); // call each frame
|
||||
*/
|
||||
|
||||
const LODSystem = (() => {
|
||||
let _scene = null;
|
||||
let _camera = null;
|
||||
let _registered = new Map(); // userId -> { mesh, sprite, distance }
|
||||
let _spriteMaterial = null;
|
||||
let _frustum = new THREE.Frustum();
|
||||
let _projScreenMatrix = new THREE.Matrix4();
|
||||
|
||||
// Thresholds
|
||||
const LOD_NEAR = 15; // Full mesh within 15 units
|
||||
const LOD_FAR = 40; // Billboard beyond 40 units
|
||||
const LOD_CULL = 80; // Don't render beyond 80 units
|
||||
const SPRITE_SIZE = 1.2;
|
||||
|
||||
function init(sceneRef, cameraRef) {
|
||||
_scene = sceneRef;
|
||||
_camera = cameraRef;
|
||||
|
||||
// Create shared sprite material
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
// Simple avatar indicator: colored circle
|
||||
ctx.fillStyle = '#00ffcc';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2); // head
|
||||
ctx.fill();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
_spriteMaterial = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthTest: true,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
|
||||
console.log('[LODSystem] Initialized');
|
||||
}
|
||||
|
||||
function registerAvatar(avatarMesh, userId, color) {
|
||||
// Create billboard sprite for this avatar
|
||||
const spriteMat = _spriteMaterial.clone();
|
||||
if (color) {
|
||||
// Tint sprite to match avatar color
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
spriteMat.map = new THREE.CanvasTexture(canvas);
|
||||
spriteMat.map.needsUpdate = true;
|
||||
}
|
||||
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.scale.set(SPRITE_SIZE, SPRITE_SIZE, 1);
|
||||
sprite.visible = false;
|
||||
_scene.add(sprite);
|
||||
|
||||
_registered.set(userId, {
|
||||
mesh: avatarMesh,
|
||||
sprite: sprite,
|
||||
distance: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterAvatar(userId) {
|
||||
const entry = _registered.get(userId);
|
||||
if (entry) {
|
||||
_scene.remove(entry.sprite);
|
||||
entry.sprite.material.dispose();
|
||||
_registered.delete(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function setSpriteColor(userId, color) {
|
||||
const entry = _registered.get(userId);
|
||||
if (!entry) return;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
entry.sprite.material.map = new THREE.CanvasTexture(canvas);
|
||||
entry.sprite.material.map.needsUpdate = true;
|
||||
}
|
||||
|
||||
function update(playerPos) {
|
||||
if (!_camera) return;
|
||||
|
||||
// Update frustum for culling
|
||||
_projScreenMatrix.multiplyMatrices(
|
||||
_camera.projectionMatrix,
|
||||
_camera.matrixWorldInverse
|
||||
);
|
||||
_frustum.setFromProjectionMatrix(_projScreenMatrix);
|
||||
|
||||
_registered.forEach((entry, userId) => {
|
||||
if (!entry.mesh) return;
|
||||
|
||||
const meshPos = entry.mesh.position;
|
||||
const distance = playerPos.distanceTo(meshPos);
|
||||
entry.distance = distance;
|
||||
|
||||
// Beyond cull distance: hide everything
|
||||
if (distance > LOD_CULL) {
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if in camera frustum
|
||||
const inFrustum = _frustum.containsPoint(meshPos);
|
||||
if (!inFrustum) {
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// LOD switching
|
||||
if (distance <= LOD_NEAR) {
|
||||
// Near: full mesh
|
||||
entry.mesh.visible = true;
|
||||
entry.sprite.visible = false;
|
||||
} else if (distance <= LOD_FAR) {
|
||||
// Mid: mesh with reduced detail (keep mesh visible)
|
||||
entry.mesh.visible = true;
|
||||
entry.sprite.visible = false;
|
||||
} else {
|
||||
// Far: billboard sprite
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = true;
|
||||
entry.sprite.position.copy(meshPos);
|
||||
entry.sprite.position.y += 1.2; // above avatar center
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getStats() {
|
||||
let meshCount = 0;
|
||||
let spriteCount = 0;
|
||||
let culledCount = 0;
|
||||
_registered.forEach(entry => {
|
||||
if (entry.mesh.visible) meshCount++;
|
||||
else if (entry.sprite.visible) spriteCount++;
|
||||
else culledCount++;
|
||||
});
|
||||
return { total: _registered.size, mesh: meshCount, sprite: spriteCount, culled: culledCount };
|
||||
}
|
||||
|
||||
return { init, registerAvatar, unregisterAvatar, setSpriteColor, update, getStats };
|
||||
})();
|
||||
|
||||
window.LODSystem = LODSystem;
|
||||
Reference in New Issue
Block a user