okb 1.1.0__tar.gz → 1.1.1__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.
- {okb-1.1.0 → okb-1.1.1}/PKG-INFO +9 -3
- {okb-1.1.0 → okb-1.1.1}/README.md +8 -2
- {okb-1.1.0 → okb-1.1.1}/okb/http_server.py +130 -88
- {okb-1.1.0 → okb-1.1.1}/pyproject.toml +1 -1
- {okb-1.1.0 → okb-1.1.1}/okb/__init__.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/cli.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/config.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/data/init.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/ingest.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/__init__.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/analyze.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/base.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/cache.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/consolidate.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/enrich.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/extractors/__init__.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/extractors/base.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/extractors/cross_doc.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/extractors/dedup.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/extractors/entity.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/extractors/todo.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/filter.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/llm/providers.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/local_embedder.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/mcp_server.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrate.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0001.initial-schema.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0002.sync-state.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0003.structured-fields.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0004.tokens.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0005.database-metadata.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0006.llm-cache.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0008.enrichment.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0009.entity-consolidation.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/migrations/0010.token-id.sql +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/modal_embedder.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/modal_llm.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/__init__.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/base.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/registry.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/sources/__init__.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/sources/dropbox_paper.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/sources/github.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/plugins/sources/todoist.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/rescan.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/scripts/__init__.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/scripts/watch.py +0 -0
- {okb-1.1.0 → okb-1.1.1}/okb/tokens.py +0 -0
{okb-1.1.0 → okb-1.1.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: okb
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Personal knowledge base with semantic search for LLMs
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -293,14 +293,20 @@ okb token create --db default -d "Claude Code"
|
|
|
293
293
|
okb serve --http --host 0.0.0.0 --port 8080
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
The server uses Streamable HTTP transport (RFC 9728 compliant):
|
|
297
|
+
- `POST /mcp` - Send JSON-RPC messages, receive SSE response
|
|
298
|
+
- `GET /mcp` - Establish SSE connection for server notifications
|
|
299
|
+
- `DELETE /mcp` - Terminate session
|
|
300
|
+
- `/sse` is an alias for `/mcp` for backward compatibility
|
|
301
|
+
|
|
302
|
+
Configure your MCP client to connect:
|
|
297
303
|
|
|
298
304
|
```json
|
|
299
305
|
{
|
|
300
306
|
"mcpServers": {
|
|
301
307
|
"knowledge-base": {
|
|
302
308
|
"type": "sse",
|
|
303
|
-
"url": "http://localhost:8080/
|
|
309
|
+
"url": "http://localhost:8080/mcp",
|
|
304
310
|
"headers": {
|
|
305
311
|
"Authorization": "Bearer okb_default_rw_a1b2c3d4e5f6g7h8"
|
|
306
312
|
}
|
|
@@ -244,14 +244,20 @@ okb token create --db default -d "Claude Code"
|
|
|
244
244
|
okb serve --http --host 0.0.0.0 --port 8080
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
The server uses Streamable HTTP transport (RFC 9728 compliant):
|
|
248
|
+
- `POST /mcp` - Send JSON-RPC messages, receive SSE response
|
|
249
|
+
- `GET /mcp` - Establish SSE connection for server notifications
|
|
250
|
+
- `DELETE /mcp` - Terminate session
|
|
251
|
+
- `/sse` is an alias for `/mcp` for backward compatibility
|
|
252
|
+
|
|
253
|
+
Configure your MCP client to connect:
|
|
248
254
|
|
|
249
255
|
```json
|
|
250
256
|
{
|
|
251
257
|
"mcpServers": {
|
|
252
258
|
"knowledge-base": {
|
|
253
259
|
"type": "sse",
|
|
254
|
-
"url": "http://localhost:8080/
|
|
260
|
+
"url": "http://localhost:8080/mcp",
|
|
255
261
|
"headers": {
|
|
256
262
|
"Authorization": "Bearer okb_default_rw_a1b2c3d4e5f6g7h8"
|
|
257
263
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""HTTP transport server for MCP with token authentication.
|
|
2
2
|
|
|
3
|
-
This module provides an HTTP server that serves the
|
|
4
|
-
token-based authentication. Tokens can be
|
|
5
|
-
or query parameter. A single HTTP server
|
|
6
|
-
with the token determining which database to use.
|
|
3
|
+
This module provides an HTTP server that serves the OKB MCP server with
|
|
4
|
+
token-based authentication using Streamable HTTP transport. Tokens can be
|
|
5
|
+
passed via Authorization header or query parameter. A single HTTP server
|
|
6
|
+
can serve multiple databases, with the token determining which database to use.
|
|
7
|
+
|
|
8
|
+
Transport: Streamable HTTP (RFC 9728 compliant)
|
|
9
|
+
- POST /mcp → send JSON-RPC messages, get SSE response
|
|
10
|
+
- GET /mcp → optional standalone SSE for server notifications
|
|
11
|
+
- DELETE /mcp → terminate session
|
|
12
|
+
- Session ID in Mcp-Session-Id header
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
from __future__ import annotations
|
|
@@ -12,12 +18,11 @@ import sys
|
|
|
12
18
|
from typing import Any
|
|
13
19
|
|
|
14
20
|
from mcp.server import Server
|
|
15
|
-
from mcp.server.
|
|
21
|
+
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
16
22
|
from mcp.types import CallToolResult, TextContent, Tool
|
|
17
|
-
from starlette.
|
|
23
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
18
24
|
from starlette.requests import Request
|
|
19
|
-
from starlette.responses import JSONResponse
|
|
20
|
-
from starlette.routing import Mount, Route
|
|
25
|
+
from starlette.responses import JSONResponse
|
|
21
26
|
|
|
22
27
|
from .config import config
|
|
23
28
|
from .local_embedder import warmup
|
|
@@ -81,14 +86,14 @@ def extract_token(request: Request) -> str | None:
|
|
|
81
86
|
|
|
82
87
|
|
|
83
88
|
class HTTPMCPServer:
|
|
84
|
-
"""HTTP server for MCP with token authentication."""
|
|
89
|
+
"""HTTP server for MCP with token authentication using Streamable HTTP transport."""
|
|
85
90
|
|
|
86
91
|
def __init__(self):
|
|
87
92
|
self.knowledge_bases: dict[str, KnowledgeBase] = {}
|
|
88
93
|
self.server = Server("knowledge-base")
|
|
89
|
-
#
|
|
90
|
-
self.
|
|
91
|
-
# Map
|
|
94
|
+
# Session manager handles all transport complexity
|
|
95
|
+
self.session_manager = StreamableHTTPSessionManager(app=self.server)
|
|
96
|
+
# Map mcp-session-id -> token_info
|
|
92
97
|
self.session_tokens: dict[str, TokenInfo] = {}
|
|
93
98
|
self._setup_handlers()
|
|
94
99
|
|
|
@@ -563,95 +568,131 @@ class HTTPMCPServer:
|
|
|
563
568
|
except Exception as e:
|
|
564
569
|
return CallToolResult(content=[TextContent(type="text", text=f"Error: {e!s}")])
|
|
565
570
|
|
|
566
|
-
def create_app(self)
|
|
571
|
+
def create_app(self):
|
|
567
572
|
"""Create the Starlette application."""
|
|
568
573
|
verifier = OKBTokenVerifier(self._get_db_url)
|
|
574
|
+
session_header_name = "mcp-session-id"
|
|
569
575
|
|
|
570
|
-
|
|
571
|
-
"""
|
|
572
|
-
# Verify token
|
|
573
|
-
token = extract_token(request)
|
|
574
|
-
if not token:
|
|
575
|
-
return JSONResponse(
|
|
576
|
-
{"error": "Missing token. Use Authorization header or ?token= parameter"},
|
|
577
|
-
status_code=401,
|
|
578
|
-
)
|
|
576
|
+
def create_mcp_handler():
|
|
577
|
+
"""Create an ASGI handler for MCP with auth."""
|
|
579
578
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
{"error": "Invalid or expired token"},
|
|
584
|
-
status_code=401,
|
|
585
|
-
)
|
|
579
|
+
async def handle_mcp(scope, receive, send):
|
|
580
|
+
"""Handle all MCP requests (GET, POST, DELETE) with auth."""
|
|
581
|
+
request = Request(scope, receive)
|
|
586
582
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
# Find the new session ID by comparing before/after
|
|
594
|
-
current_sessions = set(self.transport._read_stream_writers.keys())
|
|
595
|
-
new_sessions = current_sessions - existing_sessions
|
|
596
|
-
if not new_sessions:
|
|
597
|
-
return JSONResponse(
|
|
598
|
-
{"error": "Failed to establish session"},
|
|
599
|
-
status_code=500,
|
|
583
|
+
# Extract and verify token
|
|
584
|
+
token = extract_token(request)
|
|
585
|
+
if not token:
|
|
586
|
+
response = JSONResponse(
|
|
587
|
+
{"error": "Missing token. Use Authorization header or ?token= param"},
|
|
588
|
+
status_code=401,
|
|
600
589
|
)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
await self.server.run(
|
|
610
|
-
read_stream, write_stream, self.server.create_initialization_options()
|
|
590
|
+
await response(scope, receive, send)
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
token_info = verifier.verify(token)
|
|
594
|
+
if not token_info:
|
|
595
|
+
response = JSONResponse(
|
|
596
|
+
{"error": "Invalid or expired token"},
|
|
597
|
+
status_code=401,
|
|
611
598
|
)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
await response(scope, receive, send)
|
|
633
|
-
return
|
|
599
|
+
await response(scope, receive, send)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
# Check if this is an existing session
|
|
603
|
+
session_id = request.headers.get(session_header_name)
|
|
604
|
+
if session_id:
|
|
605
|
+
# Verify token matches existing session (compare by hash and db, not object)
|
|
606
|
+
existing_token = self.session_tokens.get(session_id)
|
|
607
|
+
if existing_token:
|
|
608
|
+
# Token must match the one used to create the session
|
|
609
|
+
if (
|
|
610
|
+
existing_token.token_hash != token_info.token_hash
|
|
611
|
+
or existing_token.database != token_info.database
|
|
612
|
+
):
|
|
613
|
+
response = JSONResponse(
|
|
614
|
+
{"error": "Token mismatch for session"},
|
|
615
|
+
status_code=401,
|
|
616
|
+
)
|
|
617
|
+
await response(scope, receive, send)
|
|
618
|
+
return
|
|
634
619
|
|
|
635
|
-
|
|
636
|
-
|
|
620
|
+
# Set current token info for tool calls
|
|
621
|
+
self.server._current_token_info = token_info
|
|
637
622
|
|
|
638
|
-
|
|
623
|
+
# Wrap send to capture the session ID from response headers
|
|
624
|
+
captured_session_id = None
|
|
639
625
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
626
|
+
async def send_wrapper(message):
|
|
627
|
+
nonlocal captured_session_id
|
|
628
|
+
if message["type"] == "http.response.start":
|
|
629
|
+
headers = message.get("headers", [])
|
|
630
|
+
for name, value in headers:
|
|
631
|
+
header_name = (
|
|
632
|
+
name.lower() if isinstance(name, bytes) else name.lower().encode()
|
|
633
|
+
)
|
|
634
|
+
if header_name == session_header_name.encode():
|
|
635
|
+
captured_session_id = (
|
|
636
|
+
value.decode() if isinstance(value, bytes) else value
|
|
637
|
+
)
|
|
638
|
+
# Store immediately since SSE keeps connection open
|
|
639
|
+
if captured_session_id not in self.session_tokens:
|
|
640
|
+
self.session_tokens[captured_session_id] = token_info
|
|
641
|
+
break
|
|
642
|
+
await send(message)
|
|
643
|
+
|
|
644
|
+
# Delegate to session manager
|
|
645
|
+
await self.session_manager.handle_request(scope, receive, send_wrapper)
|
|
646
|
+
|
|
647
|
+
return handle_mcp
|
|
648
|
+
|
|
649
|
+
# Create the MCP handler ASGI app
|
|
650
|
+
mcp_handler = create_mcp_handler()
|
|
651
|
+
|
|
652
|
+
# Custom ASGI app that routes /mcp and /sse to MCP handler
|
|
653
|
+
async def router(scope, receive, send):
|
|
654
|
+
if scope["type"] == "http":
|
|
655
|
+
path = scope["path"].rstrip("/") # Handle trailing slash
|
|
656
|
+
if path in ("/mcp", "/sse"):
|
|
657
|
+
await mcp_handler(scope, receive, send)
|
|
658
|
+
return
|
|
659
|
+
elif path == "/health" or scope["path"] == "/health":
|
|
660
|
+
response = JSONResponse({"status": "ok"})
|
|
661
|
+
await response(scope, receive, send)
|
|
662
|
+
return
|
|
663
|
+
# 404 for unknown paths
|
|
664
|
+
response = JSONResponse({"error": "Not found"}, status_code=404)
|
|
665
|
+
await response(scope, receive, send)
|
|
666
|
+
|
|
667
|
+
# Wrap with lifespan handling
|
|
668
|
+
async def app_with_lifespan(scope, receive, send):
|
|
669
|
+
if scope["type"] == "lifespan":
|
|
670
|
+
async with self.session_manager.run():
|
|
671
|
+
# Handle lifespan protocol
|
|
672
|
+
while True:
|
|
673
|
+
message = await receive()
|
|
674
|
+
if message["type"] == "lifespan.startup":
|
|
675
|
+
await send({"type": "lifespan.startup.complete"})
|
|
676
|
+
elif message["type"] == "lifespan.shutdown":
|
|
677
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
678
|
+
return
|
|
679
|
+
else:
|
|
680
|
+
await router(scope, receive, send)
|
|
643
681
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
682
|
+
# Add CORS for browser clients - wrap the raw ASGI app
|
|
683
|
+
app = CORSMiddleware(
|
|
684
|
+
app_with_lifespan,
|
|
685
|
+
allow_origins=["*"],
|
|
686
|
+
allow_methods=["GET", "POST", "DELETE"],
|
|
687
|
+
allow_headers=["Authorization", "Content-Type", session_header_name],
|
|
688
|
+
expose_headers=[session_header_name],
|
|
689
|
+
)
|
|
649
690
|
|
|
650
|
-
return
|
|
691
|
+
return app
|
|
651
692
|
|
|
652
693
|
|
|
653
694
|
def run_http_server(host: str = "127.0.0.1", port: int = 8080):
|
|
654
|
-
"""Run the HTTP MCP server."""
|
|
695
|
+
"""Run the HTTP MCP server with Streamable HTTP transport."""
|
|
655
696
|
import uvicorn
|
|
656
697
|
|
|
657
698
|
print("Warming up embedding model...", file=sys.stderr)
|
|
@@ -662,8 +703,9 @@ def run_http_server(host: str = "127.0.0.1", port: int = 8080):
|
|
|
662
703
|
app = http_server.create_app()
|
|
663
704
|
|
|
664
705
|
print(f"Starting HTTP MCP server on http://{host}:{port}", file=sys.stderr)
|
|
665
|
-
print("
|
|
666
|
-
print("
|
|
706
|
+
print(" MCP endpoint: /mcp (GET, POST, DELETE)", file=sys.stderr)
|
|
707
|
+
print(" MCP endpoint: /sse (alias for /mcp)", file=sys.stderr)
|
|
667
708
|
print(" Health endpoint: /health", file=sys.stderr)
|
|
709
|
+
print(" Transport: Streamable HTTP", file=sys.stderr)
|
|
668
710
|
|
|
669
711
|
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|