hebbianvault-mcp 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hebbianvault_mcp-0.2.0/.gitignore +27 -0
- hebbianvault_mcp-0.2.0/PKG-INFO +92 -0
- hebbianvault_mcp-0.2.0/README.md +80 -0
- hebbianvault_mcp-0.2.0/conftest.py +12 -0
- hebbianvault_mcp-0.2.0/pyproject.toml +56 -0
- hebbianvault_mcp-0.2.0/src/hebbianvault_mcp/__init__.py +19 -0
- hebbianvault_mcp-0.2.0/src/hebbianvault_mcp/client.py +118 -0
- hebbianvault_mcp-0.2.0/src/hebbianvault_mcp/config.py +106 -0
- hebbianvault_mcp-0.2.0/src/hebbianvault_mcp/server.py +109 -0
- hebbianvault_mcp-0.2.0/src/hebbianvault_mcp/tools.py +512 -0
- hebbianvault_mcp-0.2.0/tests/__init__.py +1 -0
- hebbianvault_mcp-0.2.0/tests/test_config.py +79 -0
- hebbianvault_mcp-0.2.0/tests/test_tools.py +273 -0
- hebbianvault_mcp-0.2.0/uv.lock +830 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Node
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
*.js.map
|
|
5
|
+
*.d.ts.map
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.eggs/
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
*.dist-info/
|
|
15
|
+
.venv/
|
|
16
|
+
.uv/
|
|
17
|
+
|
|
18
|
+
# Test / coverage artifacts
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
|
|
25
|
+
# IDE
|
|
26
|
+
.vscode/
|
|
27
|
+
.idea/
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hebbianvault-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Customer-installable MCP server for the Hebbian tenant brain (Python). Same 8 tools as the Node.js sibling @hebbianvault/mcp.
|
|
5
|
+
Author-email: Hebbian <hello@hebbian.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: ai-tools,hebbian,knowledge-graph,mcp,model-context-protocol
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: mcp>=1.0.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# hebbianvault-mcp (Python)
|
|
14
|
+
|
|
15
|
+
Python sibling of `@hebbianvault/mcp`. Connect a Python MCP host to your Hebbian workspace — same 8 tools, same configuration. A thin client; all intelligence and access control live in the Hebbian service.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install hebbianvault-mcp
|
|
21
|
+
# or with uv (preferred)
|
|
22
|
+
uv add hebbianvault-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Generate a token from your Hebbian integrations page (AI Tools tab → Generate token).
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Set `HEBBIAN_API_TOKEN` (or `HEBBIAN_TOKEN`) env var:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
HEBBIAN_API_TOKEN=your_token_here hebbian-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or write `~/.config/hebbian/mcp-tenant.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"token": "your_token_here"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Variable | Purpose |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `HEBBIAN_API_TOKEN` | Your token (required). `HEBBIAN_TOKEN` is also accepted. |
|
|
46
|
+
| `HEBBIAN_API_URL` | Override the API base URL (Enterprise self-host). Defaults to the Hebbian SaaS API. |
|
|
47
|
+
| `HEBBIAN_TENANT` | Optional workspace slug — only needed if your account belongs to more than one workspace. |
|
|
48
|
+
|
|
49
|
+
## Generic agent config
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import os
|
|
53
|
+
os.environ["HEBBIAN_API_TOKEN"] = "your_token_here"
|
|
54
|
+
from hebbianvault_mcp import create_server
|
|
55
|
+
server = create_server()
|
|
56
|
+
# server.run() to start serving over stdio
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Token scope
|
|
60
|
+
|
|
61
|
+
Your token decides what the adapter can see and do — a personal-workspace token gives access to your own knowledge; a company-workspace token gives access to the shared company workspace (where your role allows). Scope is decided by the Hebbian service.
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
| Tool | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `hebbian_read_node` | Read a single node by UUID |
|
|
68
|
+
| `hebbian_search` | Find nodes in your workspace matching a query |
|
|
69
|
+
| `hebbian_ask` | Ask a question and get an answer backed by source quotes |
|
|
70
|
+
| `hebbian_capture` | Write a note into your workspace |
|
|
71
|
+
| `hebbian_traverse` | Explore nodes connected to a starting node |
|
|
72
|
+
| `hebbian_provenance` | See where a node's knowledge came from |
|
|
73
|
+
| `hebbian_salience` | See a node's recent activity over time |
|
|
74
|
+
| `hebbian_recent_activity` | Catch up on recent changes in your workspace |
|
|
75
|
+
|
|
76
|
+
Results only ever include what your token is allowed to see.
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv venv && uv pip install -e ".[dev]"
|
|
82
|
+
pytest
|
|
83
|
+
ruff check src/ tests/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Node.js version
|
|
87
|
+
|
|
88
|
+
The canonical package is `@hebbianvault/mcp` on npm. This Python sibling has identical tools and configuration. Use whichever matches your MCP host environment.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
Apache-2.0. See the root [LICENSE](../LICENSE) file.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# hebbianvault-mcp (Python)
|
|
2
|
+
|
|
3
|
+
Python sibling of `@hebbianvault/mcp`. Connect a Python MCP host to your Hebbian workspace — same 8 tools, same configuration. A thin client; all intelligence and access control live in the Hebbian service.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hebbianvault-mcp
|
|
9
|
+
# or with uv (preferred)
|
|
10
|
+
uv add hebbianvault-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Generate a token from your Hebbian integrations page (AI Tools tab → Generate token).
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
Set `HEBBIAN_API_TOKEN` (or `HEBBIAN_TOKEN`) env var:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
HEBBIAN_API_TOKEN=your_token_here hebbian-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or write `~/.config/hebbian/mcp-tenant.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"token": "your_token_here"
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Variable | Purpose |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `HEBBIAN_API_TOKEN` | Your token (required). `HEBBIAN_TOKEN` is also accepted. |
|
|
34
|
+
| `HEBBIAN_API_URL` | Override the API base URL (Enterprise self-host). Defaults to the Hebbian SaaS API. |
|
|
35
|
+
| `HEBBIAN_TENANT` | Optional workspace slug — only needed if your account belongs to more than one workspace. |
|
|
36
|
+
|
|
37
|
+
## Generic agent config
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import os
|
|
41
|
+
os.environ["HEBBIAN_API_TOKEN"] = "your_token_here"
|
|
42
|
+
from hebbianvault_mcp import create_server
|
|
43
|
+
server = create_server()
|
|
44
|
+
# server.run() to start serving over stdio
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Token scope
|
|
48
|
+
|
|
49
|
+
Your token decides what the adapter can see and do — a personal-workspace token gives access to your own knowledge; a company-workspace token gives access to the shared company workspace (where your role allows). Scope is decided by the Hebbian service.
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
53
|
+
| Tool | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `hebbian_read_node` | Read a single node by UUID |
|
|
56
|
+
| `hebbian_search` | Find nodes in your workspace matching a query |
|
|
57
|
+
| `hebbian_ask` | Ask a question and get an answer backed by source quotes |
|
|
58
|
+
| `hebbian_capture` | Write a note into your workspace |
|
|
59
|
+
| `hebbian_traverse` | Explore nodes connected to a starting node |
|
|
60
|
+
| `hebbian_provenance` | See where a node's knowledge came from |
|
|
61
|
+
| `hebbian_salience` | See a node's recent activity over time |
|
|
62
|
+
| `hebbian_recent_activity` | Catch up on recent changes in your workspace |
|
|
63
|
+
|
|
64
|
+
Results only ever include what your token is allowed to see.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv venv && uv pip install -e ".[dev]"
|
|
70
|
+
pytest
|
|
71
|
+
ruff check src/ tests/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Node.js version
|
|
75
|
+
|
|
76
|
+
The canonical package is `@hebbianvault/mcp` on npm. This Python sibling has identical tools and configuration. Use whichever matches your MCP host environment.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Apache-2.0. See the root [LICENSE](../LICENSE) file.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
conftest.py — ensure src/ is on sys.path for pytest.
|
|
3
|
+
|
|
4
|
+
Belt-and-braces for uv editable installs where the .pth file may not be
|
|
5
|
+
processed (Python 3.11+ + uv venv combinations). See coding-factory LEARNINGS
|
|
6
|
+
2026-05-15.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hebbianvault-mcp"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Customer-installable MCP server for the Hebbian tenant brain (Python). Same 8 tools as the Node.js sibling @hebbianvault/mcp."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["mcp", "hebbian", "knowledge-graph", "ai-tools", "model-context-protocol"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Hebbian", email = "hello@hebbian.ai" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"mcp>=1.0.0",
|
|
19
|
+
"httpx>=0.27.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.0",
|
|
25
|
+
"pytest-asyncio>=0.23",
|
|
26
|
+
"respx>=0.21",
|
|
27
|
+
"ruff>=0.5",
|
|
28
|
+
"pyright>=1.1",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
hebbian-mcp = "hebbianvault_mcp.server:main"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/hebbianvault_mcp"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py311"
|
|
39
|
+
line-length = 100
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "I", "N", "W", "ANN", "S", "B", "UP"]
|
|
43
|
+
ignore = ["ANN101", "ANN102", "S101"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.per-file-ignores]
|
|
46
|
+
"tests/**/*.py" = ["E501", "S105", "ANN401"]
|
|
47
|
+
"src/hebbianvault_mcp/client.py" = ["ANN401"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
|
|
53
|
+
[tool.pyright]
|
|
54
|
+
pythonVersion = "3.11"
|
|
55
|
+
typeCheckingMode = "basic"
|
|
56
|
+
include = ["src", "tests"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hebbianvault_mcp — Customer-installable MCP server for the Hebbian tenant brain.
|
|
3
|
+
|
|
4
|
+
One package, scope-by-token (Employee or Company). Install in any MCP-compatible
|
|
5
|
+
Python environment. Configure with a token issued from your Hebbian integrations page.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
$ hebbian-mcp
|
|
9
|
+
|
|
10
|
+
Or programmatically:
|
|
11
|
+
from hebbianvault_mcp import create_server
|
|
12
|
+
server = create_server()
|
|
13
|
+
server.run()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .server import create_server, main
|
|
17
|
+
|
|
18
|
+
__all__ = ["create_server", "main"]
|
|
19
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hebbianvault_mcp.client — HTTPS client for the Hebbian API.
|
|
3
|
+
|
|
4
|
+
- Adds Authorization: Bearer header to every request.
|
|
5
|
+
- Surfaces API errors as HebbianApiError with status_code + error_code + message.
|
|
6
|
+
- No business logic — thin transport wrapper over httpx.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
USER_AGENT = "hebbianvault-mcp/0.2.0 (Python)"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HebbianApiError(Exception):
|
|
23
|
+
"""Structured API error returned by the Hebbian API."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
status_code: int,
|
|
28
|
+
error_code: str,
|
|
29
|
+
message: str,
|
|
30
|
+
detail: Any = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
self.error_code = error_code
|
|
35
|
+
self.detail = detail
|
|
36
|
+
|
|
37
|
+
def to_tool_error(self) -> str:
|
|
38
|
+
"""Human-readable string for MCP tool error responses."""
|
|
39
|
+
if self.status_code == 401:
|
|
40
|
+
return (
|
|
41
|
+
f"Authentication failed ({self.error_code}): {self}. "
|
|
42
|
+
"Your token may be expired or revoked. "
|
|
43
|
+
"Generate a new token from the AI Tools tab in your Hebbian integrations page."
|
|
44
|
+
)
|
|
45
|
+
if self.status_code == 403:
|
|
46
|
+
return (
|
|
47
|
+
f"Permission denied ({self.error_code}): {self}. "
|
|
48
|
+
"Check that your token scope (employee/company) matches the operation."
|
|
49
|
+
)
|
|
50
|
+
if self.status_code == 404:
|
|
51
|
+
return f"Not found ({self.error_code}): {self}"
|
|
52
|
+
if self.status_code == 429:
|
|
53
|
+
return f"Rate limit exceeded ({self.error_code}): {self}. Slow down and retry."
|
|
54
|
+
return f"API error {self.status_code} ({self.error_code}): {self}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HebbianClient:
|
|
58
|
+
"""Thin async HTTP client for the Hebbian API."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, api_url: str, token: str, tenant: str | None = None) -> None:
|
|
61
|
+
# Normalise: strip trailing slash
|
|
62
|
+
self._api_url = api_url.rstrip("/")
|
|
63
|
+
self._headers = {
|
|
64
|
+
"Authorization": f"Bearer {token}",
|
|
65
|
+
"Accept": "application/json",
|
|
66
|
+
"User-Agent": USER_AGENT,
|
|
67
|
+
}
|
|
68
|
+
# Only sent when the account belongs to more than one workspace; the API
|
|
69
|
+
# resolves the single-membership case from the token alone.
|
|
70
|
+
if tenant and tenant.strip():
|
|
71
|
+
self._headers["X-Hebbian-Tenant"] = tenant.strip()
|
|
72
|
+
|
|
73
|
+
async def get(
|
|
74
|
+
self,
|
|
75
|
+
path: str,
|
|
76
|
+
params: dict[str, Any] | None = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
"""Execute a GET request and return parsed JSON."""
|
|
79
|
+
url = f"{self._api_url}{path}"
|
|
80
|
+
async with httpx.AsyncClient() as http:
|
|
81
|
+
response = await http.get(url, headers=self._headers, params=params)
|
|
82
|
+
return await self._handle_response(response)
|
|
83
|
+
|
|
84
|
+
async def post(self, path: str, body: dict[str, Any]) -> Any:
|
|
85
|
+
"""Execute a POST request and return parsed JSON."""
|
|
86
|
+
url = f"{self._api_url}{path}"
|
|
87
|
+
async with httpx.AsyncClient() as http:
|
|
88
|
+
response = await http.post(
|
|
89
|
+
url,
|
|
90
|
+
headers={**self._headers, "Content-Type": "application/json"},
|
|
91
|
+
content=json.dumps(body).encode(),
|
|
92
|
+
)
|
|
93
|
+
return await self._handle_response(response)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
async def _handle_response(response: httpx.Response) -> Any:
|
|
97
|
+
"""Parse and validate a response. Raises HebbianApiError on non-2xx."""
|
|
98
|
+
if response.is_success:
|
|
99
|
+
return response.json()
|
|
100
|
+
|
|
101
|
+
# Try to parse structured error body
|
|
102
|
+
error_code = f"HTTP_{response.status_code}"
|
|
103
|
+
message = response.reason_phrase or f"Status {response.status_code}"
|
|
104
|
+
detail = None
|
|
105
|
+
try:
|
|
106
|
+
body = response.json()
|
|
107
|
+
error_code = body.get("code") or body.get("error") or error_code
|
|
108
|
+
message = body.get("message") or body.get("error") or message
|
|
109
|
+
detail = body.get("detail")
|
|
110
|
+
except Exception: # noqa: BLE001
|
|
111
|
+
message = response.text or message
|
|
112
|
+
|
|
113
|
+
raise HebbianApiError(
|
|
114
|
+
status_code=response.status_code,
|
|
115
|
+
error_code=error_code,
|
|
116
|
+
message=message,
|
|
117
|
+
detail=detail,
|
|
118
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hebbianvault_mcp.config — Configuration loading.
|
|
3
|
+
|
|
4
|
+
Auth: bearer token from HEBBIAN_API_TOKEN env var (preferred) or
|
|
5
|
+
HEBBIAN_TOKEN (alternative) or config-file JSON path.
|
|
6
|
+
URL: HEBBIAN_API_URL env var (default: the Hebbian SaaS API).
|
|
7
|
+
Tenant: HEBBIAN_TENANT (optional) — only needed when your account belongs to
|
|
8
|
+
more than one Hebbian workspace.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Hebbian SaaS API. Enterprise/self-host customers override with HEBBIAN_API_URL.
|
|
22
|
+
DEFAULT_API_URL = "https://api.hebbianvault.com"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class HebbianConfig:
|
|
27
|
+
"""Resolved configuration for the MCP server."""
|
|
28
|
+
|
|
29
|
+
api_url: str
|
|
30
|
+
"""Base URL of the Hebbian API. Configurable for Enterprise self-host."""
|
|
31
|
+
|
|
32
|
+
token: str
|
|
33
|
+
"""Bearer token for authentication. Never log this value."""
|
|
34
|
+
|
|
35
|
+
tenant: str | None = None
|
|
36
|
+
"""Optional workspace slug; sent as X-Hebbian-Tenant when set."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_config() -> HebbianConfig:
|
|
40
|
+
"""
|
|
41
|
+
Load configuration from environment variables and optional config file.
|
|
42
|
+
|
|
43
|
+
Priority (highest first):
|
|
44
|
+
1. HEBBIAN_API_TOKEN / HEBBIAN_TOKEN env vars
|
|
45
|
+
2. HEBBIAN_CONFIG_PATH — path to a JSON config file
|
|
46
|
+
3. ~/.config/hebbian/mcp-tenant.json
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
RuntimeError: if no token can be resolved.
|
|
50
|
+
"""
|
|
51
|
+
api_url = os.environ.get("HEBBIAN_API_URL", DEFAULT_API_URL)
|
|
52
|
+
env_tenant = (os.environ.get("HEBBIAN_TENANT") or "").strip() or None
|
|
53
|
+
|
|
54
|
+
# ── Token from env ─────────────────────────────────────────────────────────
|
|
55
|
+
env_token = os.environ.get("HEBBIAN_API_TOKEN") or os.environ.get("HEBBIAN_TOKEN")
|
|
56
|
+
if env_token:
|
|
57
|
+
return HebbianConfig(api_url=api_url, token=env_token, tenant=env_tenant)
|
|
58
|
+
|
|
59
|
+
# ── Token from config file ─────────────────────────────────────────────────
|
|
60
|
+
config_path = _resolve_config_path()
|
|
61
|
+
if config_path:
|
|
62
|
+
file_cfg = _read_config_file(config_path)
|
|
63
|
+
if file_cfg.get("token"):
|
|
64
|
+
return HebbianConfig(
|
|
65
|
+
api_url=file_cfg.get("api_url", api_url),
|
|
66
|
+
token=file_cfg["token"],
|
|
67
|
+
tenant=env_tenant or file_cfg.get("tenant"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
raise RuntimeError(
|
|
71
|
+
"Hebbian MCP: No API token found. "
|
|
72
|
+
"Set HEBBIAN_API_TOKEN env var or HEBBIAN_TOKEN env var, "
|
|
73
|
+
"or write your token to ~/.config/hebbian/mcp-tenant.json as "
|
|
74
|
+
'{"token": "..."}. '
|
|
75
|
+
"Generate a token from the AI Tools tab in your Hebbian integrations page."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_config_path() -> Path | None:
|
|
80
|
+
"""Resolve config file path — explicit env var or XDG default."""
|
|
81
|
+
explicit = os.environ.get("HEBBIAN_CONFIG_PATH")
|
|
82
|
+
if explicit:
|
|
83
|
+
p = Path(explicit).expanduser().resolve()
|
|
84
|
+
if p.exists():
|
|
85
|
+
return p
|
|
86
|
+
logger.warning(
|
|
87
|
+
"HEBBIAN_CONFIG_PATH set to %r but file not found.", explicit
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
home = Path.home()
|
|
92
|
+
default = home / ".config" / "hebbian" / "mcp-tenant.json"
|
|
93
|
+
return default if default.exists() else None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_config_file(path: Path) -> dict[str, str]:
|
|
97
|
+
"""Read and parse a JSON config file; tolerates missing fields."""
|
|
98
|
+
try:
|
|
99
|
+
raw = path.read_text(encoding="utf-8")
|
|
100
|
+
parsed = json.loads(raw)
|
|
101
|
+
if not isinstance(parsed, dict):
|
|
102
|
+
raise ValueError("Config file must be a JSON object")
|
|
103
|
+
return {k: str(v) for k, v in parsed.items() if isinstance(v, str)}
|
|
104
|
+
except Exception as exc: # noqa: BLE001
|
|
105
|
+
logger.warning("Could not read config file at %r: %s", str(path), exc)
|
|
106
|
+
return {}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
hebbianvault_mcp.server — MCP server entry point.
|
|
4
|
+
|
|
5
|
+
Boots the Hebbian tenant MCP server over stdio transport.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m hebbianvault_mcp.server
|
|
9
|
+
hebbian-mcp # via pyproject.toml [project.scripts]
|
|
10
|
+
|
|
11
|
+
Auth:
|
|
12
|
+
HEBBIAN_API_TOKEN env var (or HEBBIAN_TOKEN)
|
|
13
|
+
or ~/.config/hebbian/mcp-tenant.json with {"token": "hbn_..."}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from mcp.server import Server
|
|
24
|
+
from mcp.server.stdio import stdio_server
|
|
25
|
+
from mcp.types import TextContent, Tool
|
|
26
|
+
|
|
27
|
+
from .client import HebbianClient
|
|
28
|
+
from .config import load_config
|
|
29
|
+
from .tools import TOOL_HANDLERS, TOOL_SCHEMAS
|
|
30
|
+
|
|
31
|
+
logging.basicConfig(
|
|
32
|
+
level=logging.WARNING,
|
|
33
|
+
stream=sys.stderr,
|
|
34
|
+
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
|
|
35
|
+
)
|
|
36
|
+
logger = logging.getLogger("hebbianvault_mcp")
|
|
37
|
+
|
|
38
|
+
SERVER_NAME = "hebbianvault-mcp"
|
|
39
|
+
SERVER_VERSION = "0.1.0"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_server() -> Server:
|
|
43
|
+
"""
|
|
44
|
+
Create and configure the Hebbian MCP server.
|
|
45
|
+
|
|
46
|
+
Call server.run() or use as an async context to start serving.
|
|
47
|
+
"""
|
|
48
|
+
config = load_config()
|
|
49
|
+
client = HebbianClient(api_url=config.api_url, token=config.token, tenant=config.tenant)
|
|
50
|
+
|
|
51
|
+
app = Server(SERVER_NAME)
|
|
52
|
+
|
|
53
|
+
@app.list_tools()
|
|
54
|
+
async def list_tools() -> list[Tool]: # type: ignore[override]
|
|
55
|
+
return [
|
|
56
|
+
Tool(
|
|
57
|
+
name=schema["name"],
|
|
58
|
+
description=schema["description"],
|
|
59
|
+
inputSchema=schema["inputSchema"],
|
|
60
|
+
)
|
|
61
|
+
for schema in TOOL_SCHEMAS
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
@app.call_tool()
|
|
65
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: # type: ignore[override]
|
|
66
|
+
handler = TOOL_HANDLERS.get(name)
|
|
67
|
+
if handler is None:
|
|
68
|
+
return [
|
|
69
|
+
TextContent(
|
|
70
|
+
type="text",
|
|
71
|
+
text=f"Unknown tool: {name}. Available tools: {', '.join(TOOL_HANDLERS)}",
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
try:
|
|
75
|
+
result = await handler(client, arguments or {})
|
|
76
|
+
return [TextContent(type="text", text=result)]
|
|
77
|
+
except Exception as exc: # noqa: BLE001
|
|
78
|
+
logger.error("Tool %r raised: %s", name, exc)
|
|
79
|
+
return [TextContent(type="text", text=f"Error: {exc}")]
|
|
80
|
+
|
|
81
|
+
logger.info(
|
|
82
|
+
"%s@%s started. API: %s",
|
|
83
|
+
SERVER_NAME,
|
|
84
|
+
SERVER_VERSION,
|
|
85
|
+
config.api_url,
|
|
86
|
+
)
|
|
87
|
+
return app
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
"""Entry point for `hebbian-mcp` CLI command."""
|
|
92
|
+
server = create_server()
|
|
93
|
+
|
|
94
|
+
async def _run() -> None:
|
|
95
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
96
|
+
await server.run(
|
|
97
|
+
read_stream,
|
|
98
|
+
write_stream,
|
|
99
|
+
server.create_initialization_options(),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
asyncio.run(_run())
|
|
104
|
+
except KeyboardInterrupt:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|