-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathapp.py
More file actions
171 lines (140 loc) · 6.31 KB
/
Copy pathapp.py
File metadata and controls
171 lines (140 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path
import dotenv
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.responses import JSONResponse
from starlette.routing import Mount, Route
_project_root = Path(__file__).resolve().parent.parent.parent.parent
dotenv.load_dotenv(_project_root / ".env")
from contextlib import asynccontextmanager
from src.lib.tracing.middleware import TraceMiddleware
from src.lib.tracing.runtime import start_runtime_monitoring, stop_runtime_monitoring
from src.services.mcp.main import auth_token_var, mcp, oauth_provider
PLUGINS_DIR = Path(os.getenv("PLUGINS_DIR", str(_project_root / "plugins")))
logger = logging.getLogger("brainapi.plugins")
def _load_mcp_plugins():
from src.core.plugins.context import PluginContext
from src.core.plugins.loader import PluginLoader
ctx = PluginContext.from_mcp(mcp)
loader = PluginLoader(plugins_dir=PLUGINS_DIR, context=ctx)
results = loader.load_all()
_log_plugin_banner(loader, results)
def _log_plugin_banner(loader, results: dict[str, bool]):
loaded = loader.loaded_plugins
total = len(results)
ok = sum(1 for v in results.values() if v)
failed = total - ok
lines = [
"",
"\033[35m ╔══════════════════════════════════════════════════════╗\033[0m",
"\033[35m ║\033[0m \033[1;35m⚡ BrainAPI MCP Plugin System ⚡\033[0m \033[35m║\033[0m",
"\033[35m ╠══════════════════════════════════════════════════════╣\033[0m",
]
if total == 0:
lines.append(
"\033[35m ║\033[0m \033[2mNo plugins installed\033[0m \033[35m║\033[0m"
)
else:
for name, success in results.items():
manifest = loaded.get(name)
if success and manifest:
ver = f"v{manifest.version}"
status = "\033[32m✔ loaded\033[0m"
label = f"{manifest.name} ({ver})"
else:
status = "\033[31m✘ failed\033[0m"
label = name
padded = f" {status} {label}"
visible_len = len(f" ✔ loaded {label}")
pad = 54 - visible_len
lines.append(f"\033[35m ║\033[0m{padded}{' ' * max(pad, 1)}\033[35m║\033[0m")
lines.append("\033[35m ╠══════════════════════════════════════════════════════╣\033[0m")
summary_parts = [f"\033[1;32m{ok} loaded\033[0m"]
if failed:
summary_parts.append(f"\033[1;31m{failed} failed\033[0m")
summary_text = f" {' · '.join(summary_parts)}"
visible_summary_len = len(f" {ok} loaded" + (f" · {failed} failed" if failed else ""))
summary_pad = 54 - visible_summary_len
lines.append(f"\033[35m ║\033[0m{summary_text}{' ' * max(summary_pad, 1)}\033[35m║\033[0m")
lines.append("\033[35m ╚══════════════════════════════════════════════════════╝\033[0m")
lines.append("")
print("\n".join(lines))
_load_mcp_plugins()
_mcp_app = mcp.streamable_http_app()
@asynccontextmanager
async def _lifespan(app):
start_runtime_monitoring("brainapi-mcp")
async with _mcp_app.router.lifespan_context(app):
try:
yield
finally:
stop_runtime_monitoring("brainapi-mcp")
class AuthContextMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] in ("http", "websocket"):
raw_headers = list(scope.get("headers", []))
headers = dict(raw_headers)
token = None
brainpat = headers.get(b"brainpat")
if brainpat and b"authorization" not in headers:
raw_headers.append((b"authorization", b"Bearer " + brainpat))
scope = {**scope, "headers": raw_headers}
if brainpat:
token = brainpat.decode()
else:
raw = (headers.get(b"authorization") or b"").decode()
bearer = None
if raw.startswith("Bearer: "):
bearer = raw.removeprefix("Bearer: ").strip() or None
if bearer:
raw_headers = [
(name, value)
for name, value in raw_headers
if name.lower() != b"authorization"
]
raw_headers.append((b"authorization", f"Bearer {bearer}".encode()))
scope = {**scope, "headers": raw_headers}
elif raw.startswith("Bearer "):
bearer = raw.removeprefix("Bearer ").strip() or None
if bearer:
if oauth_provider:
pat = oauth_provider.get_pat_for_access_token(bearer)
token = pat if pat else bearer
else:
token = bearer
auth_token_var.set(token)
await self.app(scope, receive, send)
async def _health(_request):
return JSONResponse({"status": "ok"}, status_code=200)
async def _mcp_info(_request):
body = {
"service": "brainapi-mcp",
"streamable_http": True,
"path": "/mcp",
}
if oauth_provider:
body["oauth"] = True
body["oauth_consent_path"] = "/mcp-oauth/consent"
return JSONResponse(body, status_code=200)
@asynccontextmanager
async def _lifespan(app):
async with _mcp_app.router.lifespan_context(_mcp_app):
yield
_custom_routes = [
Route("/", _health, methods=["GET"]),
Route("/mcp", _mcp_info, methods=["GET"]),
Route("/mcp/info", _mcp_info, methods=["GET"]),
]
app = Starlette(
routes=_custom_routes + [Mount("/", app=_mcp_app)],
middleware=[
Middleware(TraceMiddleware, service_name="brainapi-mcp"),
Middleware(AuthContextMiddleware),
],
lifespan=_lifespan,
)