Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:36:21 +02:00
commit cb5be898ea
663 files changed
+82674

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Connectors
Reserved for target-specific, read-only connector implementations. Connectors
must not import peer connectors or access the Hub database directly.
+1
View File
@@ -0,0 +1 @@
"""First-party connector implementations."""
+13
View File
@@ -0,0 +1,13 @@
"""Gitea read-only connector package."""
from .changesets import DulwichChangesetWorkspace
from .fake import FakeGiteaServer
from .server import GITEA_RESOURCES, GITEA_TOOLS, GiteaConnector
__all__ = [
"DulwichChangesetWorkspace",
"FakeGiteaServer",
"GITEA_RESOURCES",
"GITEA_TOOLS",
"GiteaConnector",
]
+341
View File
@@ -0,0 +1,341 @@
"""Bounded patching and single-commit Git workspaces for Gitea changesets."""
from __future__ import annotations
import io
import os
import re
import tempfile
from collections.abc import Iterator, Mapping, Sequence
from contextlib import contextmanager
from pathlib import Path
from threading import Lock
from typing import Any, Protocol
from dulwich import porcelain
from dulwich.repo import Repo
from packages.connector_kit import GiteaError, RegisteredRepository
from packages.connector_kit.gitea import TokenProvider
_HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$")
_SHA = re.compile(r"^[0-9a-fA-F]{40,64}$")
_MAX_CHANGESET_BYTES = 1_000_000
_MAX_OPERATIONS = 50
_MAX_WORKSPACE_BYTES = 256 * 1024 * 1024
_DULWICH_IDENTITY_LOCK = Lock()
_DULWICH_IDENTITY = {"USER": "mcp-agent", "EMAIL": "mcp-agent@example.com"}
@contextmanager
def _dulwich_clone_identity() -> Iterator[None]:
"""Give Dulwich a bounded reflog identity without mutating process state concurrently."""
with _DULWICH_IDENTITY_LOCK:
previous = {name: os.environ.get(name) for name in _DULWICH_IDENTITY}
os.environ.update(_DULWICH_IDENTITY)
try:
yield
finally:
for name, value in previous.items():
if value is None:
os.environ.pop(name, None)
else:
os.environ[name] = value
def _hex_object_id(value: bytes) -> str:
try:
decoded = value.decode("ascii")
except UnicodeDecodeError:
return value.hex()
return decoded.casefold() if _SHA.fullmatch(decoded) else value.hex()
class ChangesetWorkspace(Protocol):
def apply(
self,
repository: RegisteredRepository,
branch: str,
message: str,
operations: Sequence[Mapping[str, Any]],
) -> dict[str, Any]: ...
def apply_unified_patch(original: str, patch: str) -> str:
"""Apply a single-file unified diff with exact context matching."""
if not patch or len(patch.encode("utf-8")) > 300_000 or "\x00" in patch:
raise GiteaError("GITEA_PATCH_INVALID", "patch is outside the connector budget")
lines = patch.splitlines(keepends=True)
index = 0
while index < len(lines) and not lines[index].startswith("@@ "):
if lines[index].startswith(("GIT binary patch", "Binary files ")):
raise GiteaError("GITEA_PATCH_INVALID", "binary patches are not supported")
index += 1
if index == len(lines):
raise GiteaError("GITEA_PATCH_INVALID", "patch contains no unified diff hunk")
source = original.splitlines(keepends=True)
output: list[str] = []
source_index = 0
seen_hunk = False
while index < len(lines):
header = lines[index].rstrip("\r\n")
match = _HUNK.fullmatch(header)
if match is None:
raise GiteaError("GITEA_PATCH_INVALID", "patch contains an invalid hunk header")
seen_hunk = True
old_start = int(match.group(1))
old_count = int(match.group(2) or "1")
new_count = int(match.group(4) or "1")
target_index = max(old_start - 1, 0)
if target_index < source_index or target_index > len(source):
raise GiteaError("GITEA_PATCH_CONFLICT", "patch hunks overlap or exceed the file")
output.extend(source[source_index:target_index])
source_index = target_index
index += 1
consumed = 0
produced = 0
while index < len(lines) and not lines[index].startswith("@@ "):
line = lines[index]
if line.startswith("\\ No newline at end of file"):
index += 1
continue
if not line or line[0] not in {" ", "+", "-"}:
raise GiteaError("GITEA_PATCH_INVALID", "patch contains an invalid hunk line")
marker, value = line[0], line[1:]
if marker in {" ", "-"}:
if source_index >= len(source) or source[source_index] != value:
raise GiteaError("GITEA_PATCH_CONFLICT", "patch context does not match")
source_index += 1
consumed += 1
if marker in {" ", "+"}:
output.append(value)
produced += 1
index += 1
if consumed != old_count or produced != new_count:
raise GiteaError("GITEA_PATCH_INVALID", "patch hunk counts do not match")
if not seen_hunk:
raise GiteaError("GITEA_PATCH_INVALID", "patch contains no hunks")
output.extend(source[source_index:])
result = "".join(output)
if len(result.encode("utf-8")) > 200_000:
raise GiteaError("GITEA_INPUT_INVALID", "patched file exceeds the connector budget")
return result
def validate_operations(operations: Sequence[Mapping[str, Any]]) -> tuple[dict[str, Any], ...]:
if not 1 <= len(operations) <= _MAX_OPERATIONS:
raise GiteaError("GITEA_CHANGESET_INVALID", "changeset operation count is invalid")
total = 0
normalized: list[dict[str, Any]] = []
touched: set[str] = set()
for raw in operations:
kind = str(raw.get("kind", ""))
if kind not in {"create", "update", "delete", "move", "patch"}:
raise GiteaError("GITEA_CHANGESET_INVALID", "changeset operation kind is invalid")
path = str(raw.get("path", ""))
if not path or path in touched:
raise GiteaError("GITEA_CHANGESET_INVALID", "changeset paths must be unique")
touched.add(path)
operation: dict[str, Any] = {"kind": kind, "path": path}
for field in ("content", "patch", "expectedSha", "destinationPath"):
if field in raw and raw[field] is not None:
operation[field] = str(raw[field])
if kind in {"update", "delete", "move", "patch"} and not _SHA.fullmatch(
operation.get("expectedSha", "")
):
raise GiteaError("GITEA_CHANGESET_INVALID", "expectedSha is required and invalid")
if kind in {"create", "update"} and "content" not in operation:
raise GiteaError("GITEA_CHANGESET_INVALID", "content is required")
if kind == "patch" and "patch" not in operation:
raise GiteaError("GITEA_CHANGESET_INVALID", "patch text is required")
if kind == "move" and not operation.get("destinationPath"):
raise GiteaError("GITEA_CHANGESET_INVALID", "destinationPath is required")
destination = operation.get("destinationPath")
if destination is not None:
if destination in touched:
raise GiteaError("GITEA_CHANGESET_INVALID", "changeset paths must be unique")
touched.add(destination)
total += sum(len(str(value).encode("utf-8")) for value in operation.values())
normalized.append(operation)
if total > _MAX_CHANGESET_BYTES:
raise GiteaError("GITEA_CHANGESET_INVALID", "changeset exceeds the connector budget")
return tuple(normalized)
class DulwichChangesetWorkspace:
"""Apply an already-validated changeset through a fixed, isolated Git workflow."""
def __init__(self, base_url: str, token_provider: TokenProvider, token_reference: str) -> None:
self._base_url = base_url.rstrip("/")
self._token_provider = token_provider
self._token_reference = token_reference
@staticmethod
def _relative(root: Path, value: str) -> Path:
if not value or value.startswith(("/", "\\")) or "\\" in value:
raise GiteaError("GITEA_PATH_INVALID", "repository file path is invalid")
parts = value.split("/")
if any(not part or part in {".", ".."} or "\x00" in part for part in parts):
raise GiteaError("GITEA_PATH_INVALID", "repository file path is invalid")
target = root.joinpath(*parts)
try:
target.resolve(strict=False).relative_to(root.resolve())
except ValueError as exc:
raise GiteaError("GITEA_PATH_INVALID", "repository file path is invalid") from exc
return target
@staticmethod
def _blob_sha(repo: Repo, relative: str) -> str | None:
try:
entry = repo.open_index()[relative.encode("utf-8")]
except KeyError:
return None
sha = getattr(entry, "sha", None)
return _hex_object_id(sha) if sha is not None else None
def apply(
self,
repository: RegisteredRepository,
branch: str,
message: str,
operations: Sequence[Mapping[str, Any]],
) -> dict[str, Any]:
normalized = validate_operations(operations)
token = self._token_provider.get_token(self._token_reference)
remote = f"{self._base_url}/{repository.owner}/{repository.name}.git"
authenticated = remote.startswith(("https://", "http://"))
stderr = io.BytesIO()
with tempfile.TemporaryDirectory(prefix="itworx-gitea-") as temporary:
root = Path(temporary) / "repository"
repo: Repo | None = None
try:
with _dulwich_clone_identity():
if authenticated:
repo = porcelain.clone(
remote,
root,
branch=branch,
depth=1,
errstream=stderr,
username="oauth2",
password=token,
)
else:
repo = porcelain.clone(
remote, root, branch=branch, depth=1, errstream=stderr
)
workspace_bytes = sum(
entry.stat(follow_symlinks=False).st_size
for entry in root.rglob("*")
if entry.is_file() and not entry.is_symlink()
)
if workspace_bytes > _MAX_WORKSPACE_BYTES:
raise GiteaError(
"GITEA_CHANGESET_TOO_LARGE",
"repository exceeds the changeset workspace budget",
)
staged: list[str] = []
removed: list[str] = []
for operation in normalized:
kind = operation["kind"]
relative = operation["path"]
target = self._relative(root, relative)
actual_sha = self._blob_sha(repo, relative)
expected_sha = operation.get("expectedSha")
if expected_sha is not None and actual_sha != expected_sha.casefold():
raise GiteaError(
"GITEA_CHANGESET_CONFLICT", f"stale expectedSha for {relative}"
)
if kind == "create":
if actual_sha is not None or target.exists():
raise GiteaError(
"GITEA_CHANGESET_CONFLICT", f"file already exists: {relative}"
)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(operation["content"], encoding="utf-8")
staged.append(relative)
elif kind == "update":
if actual_sha is None:
raise GiteaError(
"GITEA_CHANGESET_CONFLICT", f"file does not exist: {relative}"
)
target.write_text(operation["content"], encoding="utf-8")
staged.append(relative)
elif kind == "patch":
if actual_sha is None:
raise GiteaError(
"GITEA_CHANGESET_CONFLICT", f"file does not exist: {relative}"
)
original = target.read_text(encoding="utf-8")
target.write_text(
apply_unified_patch(original, operation["patch"]), encoding="utf-8"
)
staged.append(relative)
elif kind == "delete":
if actual_sha is None:
raise GiteaError(
"GITEA_CHANGESET_CONFLICT", f"file does not exist: {relative}"
)
target.unlink()
removed.append(relative)
else:
destination = str(operation["destinationPath"])
destination_target = self._relative(root, destination)
if actual_sha is None or destination_target.exists():
raise GiteaError(
"GITEA_CHANGESET_CONFLICT", "move source/destination is invalid"
)
destination_target.parent.mkdir(parents=True, exist_ok=True)
os.replace(target, destination_target)
removed.append(relative)
staged.append(destination)
if removed:
porcelain.rm(repo, removed)
if staged:
porcelain.add(repo, staged)
status = porcelain.status(repo)
if not status.staged and not status.unstaged and not status.untracked:
raise GiteaError("GITEA_CHANGESET_EMPTY", "changeset produces no change")
commit_sha = _hex_object_id(
porcelain.commit(
repo,
message=message.encode("utf-8"),
author=b"ChatGPT MCP <mcp-agent@example.com>",
committer=b"ChatGPT MCP <mcp-agent@example.com>",
)
)
if authenticated:
porcelain.push(
repo,
remote,
refspecs=f"refs/heads/{branch}:refs/heads/{branch}",
force=False,
errstream=stderr,
username="oauth2",
password=token,
)
else:
porcelain.push(
repo,
remote,
refspecs=f"refs/heads/{branch}:refs/heads/{branch}",
force=False,
errstream=stderr,
)
except GiteaError:
raise
except Exception as exc:
raise GiteaError(
"GITEA_CHANGESET_FAILED", "bounded Git changeset could not be committed"
) from exc
finally:
if repo is not None:
repo.close()
return {
"commitSha": commit_sha,
"branch": branch,
"operationCount": len(normalized),
"atomic": True,
}
+675
View File
@@ -0,0 +1,675 @@
"""Deterministic in-memory Gitea HTTP transport for contract tests."""
from __future__ import annotations
import base64
import json
from typing import Any
from urllib.parse import parse_qs, unquote, urlparse
import httpx
from .managed_validation import (
MANAGED_VALIDATION_CONTEXT,
MANAGED_VALIDATION_PATH,
MANAGED_VALIDATION_WORKFLOW,
)
class FakeGiteaServer:
"""A tiny bounded Gitea API fixture with observable requests and no network."""
def __init__(
self,
*,
unsupported_search: bool = False,
deny_user_repositories: bool = False,
deny_protected_workflow_writes: bool = False,
redirect_to: str | None = None,
gitea_version: str = "1.26.4",
deny_merge_transition: bool = False,
) -> None:
self.unsupported_search = unsupported_search
self.deny_user_repositories = deny_user_repositories
self.deny_protected_workflow_writes = deny_protected_workflow_writes
self.redirect_to = redirect_to
self.gitea_version = gitea_version
self.deny_merge_transition = deny_merge_transition
self.recorded_methods: list[str] = []
self.recorded_authorizations: list[str] = []
self.recorded_paths: list[str] = []
self.recorded_bodies: list[dict[str, Any]] = []
self._repos = {
"acme/hub": {
"owner": {"login": "acme"},
"name": "hub",
"full_name": "acme/hub",
"description": "ITWorx MCP Hub",
"private": True,
"default_branch": "main",
"updated_at": "2026-08-01T10:00:00Z",
"token": "fixture-token-must-not-leak",
}
}
self._branches: list[dict[str, Any]] = [
{"name": "main", "commit": {"id": "a" * 40}, "protected": True},
{"name": "release", "commit": {"id": "b" * 40}, "protected": False},
{"name": "feature", "commit": {"id": "c" * 40}, "protected": False},
]
self._branch_protections: dict[str, list[dict[str, Any]]] = {
"acme/hub": [
{
"rule_name": "main",
"branch_name": "main",
"priority": 1,
"enable_push": False,
"enable_push_whitelist": False,
"push_whitelist_usernames": [],
"push_whitelist_teams": [],
"push_whitelist_deploy_keys": False,
"enable_force_push": False,
"enable_force_push_allowlist": False,
"force_push_allowlist_usernames": [],
"force_push_allowlist_teams": [],
"force_push_allowlist_deploy_keys": False,
"enable_merge_whitelist": False,
"merge_whitelist_usernames": [],
"merge_whitelist_teams": [],
"enable_bypass_allowlist": False,
"bypass_allowlist_usernames": [],
"bypass_allowlist_teams": [],
"enable_status_check": True,
"status_check_contexts": [MANAGED_VALIDATION_CONTEXT],
"required_approvals": 0,
"enable_approvals_whitelist": False,
"approvals_whitelist_username": [],
"approvals_whitelist_teams": [],
"block_on_rejected_reviews": True,
"block_on_official_review_requests": False,
"block_on_outdated_branch": True,
"dismiss_stale_approvals": True,
"ignore_stale_approvals": False,
"require_signed_commits": False,
"protected_file_patterns": "",
"unprotected_file_patterns": "",
"block_admin_merge_override": True,
}
]
}
self._managed_workflows: dict[str, str] = {"acme/hub": MANAGED_VALIDATION_WORKFLOW}
self._commits = [
{
"sha": "a" * 40,
"commit": {"message": "Add bounded connector", "author": {"name": "Ada"}},
},
{"sha": "b" * 40, "commit": {"message": "Document the API", "author": {"name": "Lin"}}},
{"sha": "c" * 40, "commit": {"message": "Add tests", "author": {"name": "Kai"}}},
]
self._compare_commits = [self._commits[0]]
self._reverse_compare_commits: list[dict[str, Any]] = []
self._releases = [
{"tag_name": "v1.0.0", "name": "First release", "draft": False, "prerelease": False}
]
self._issues = [
{
"number": 1,
"title": "Bounded read API",
"state": "open",
"html_url": "https://gitea.example/acme/hub/issues/1",
}
]
self._pulls = [
{
"number": 7,
"title": "Add governed Codex workflows",
"state": "open",
"draft": False,
"mergeable": True,
"html_url": "https://gitea.example/acme/hub/pulls/7",
"head": {"ref": "codex/workflows", "sha": "e" * 40},
"base": {"ref": "main", "sha": "a" * 40},
"updated_at": "2026-08-21T20:00:00Z",
}
]
self._pull_files = [
{
"filename": "services/runtime/mcp_server.py",
"status": "modified",
"additions": 42,
"deletions": 2,
}
]
self._statuses = [
{
"status": "success",
"context": MANAGED_VALIDATION_CONTEXT,
"description": "All checks passed",
"target_url": "https://gitea.example/acme/hub/actions/runs/42",
"created_at": "2026-08-21T20:05:00Z",
}
]
self._file = "# ITWorx MCP Hub\n\nThis is a safe fixture.\n"
self._reviews: list[dict[str, Any]] = []
self._review_comments = [
{"id": 12, "path": "README.md", "new_position": 1, "body": "Clarify this."}
]
self._action_run = {
"id": 42,
"name": "ChatGPT validation",
"status": "completed",
"conclusion": "success",
"event": "pull_request",
"head_branch": "codex/workflows",
"head_sha": "e" * 40,
"workflow_id": "managed-validation.yml",
"html_url": "https://gitea.example/acme/hub/actions/runs/42",
}
self._action_job = {
"id": 43,
"name": "full",
"status": "completed",
"conclusion": "success",
}
self._action_job_logs = "all checks passed\n"
def transport(self) -> httpx.MockTransport:
return httpx.MockTransport(self._handle)
def _handle(self, request: httpx.Request) -> httpx.Response:
self.recorded_methods.append(request.method)
self.recorded_authorizations.append(request.headers.get("authorization", ""))
if self.redirect_to is not None:
return httpx.Response(302, headers={"location": self.redirect_to}, request=request)
parsed = urlparse(str(request.url))
self.recorded_paths.append(parsed.path)
if not parsed.path.startswith("/api/v1/"):
return self._json(404, {"message": "API prefix missing"}, request)
path = parsed.path.removeprefix("/api/v1/")
query = parse_qs(parsed.query)
body: dict[str, Any] = {}
if request.content:
decoded = json.loads(request.content.decode("utf-8"))
body = decoded if isinstance(decoded, dict) else {}
self.recorded_bodies.append(body)
if path == "version" and request.method == "GET":
return self._json(200, {"version": self.gitea_version}, request)
if path == "repos/search":
if self.unsupported_search:
return self._json(501, {"message": "unsupported"}, request)
if "q" not in query:
repositories = list(self._repos.values())
if query.get("private") == ["true"]:
repositories = [item for item in repositories if item.get("private") is True]
return self._json(200, {"ok": True, "data": repositories}, request)
return self._json(
200,
{
"ok": True,
"data": [{"repository": {"full_name": "acme/hub"}, "path": "README.md"}],
},
request,
)
if path == "user" and request.method == "GET":
return self._json(200, {"login": "acme"}, request)
if path == "user/repos" and request.method == "GET":
if self.deny_user_repositories:
return self._json(403, {"message": "denied"}, request)
return self._paged(list(self._repos.values()), query, request)
if path == "user/repos" and request.method == "POST":
name = str(body.get("name", ""))
qualified_name = f"acme/{name}"
if qualified_name in self._repos:
return self._json(409, {"message": "repository already exists"}, request)
auto_init = bool(body.get("auto_init", True))
repository = {
"owner": {"login": "acme"},
"name": name,
"full_name": qualified_name,
"description": str(body.get("description", "")),
"private": bool(body.get("private", True)),
"empty": not auto_init,
"default_branch": str(body.get("default_branch", "main")) if auto_init else "",
"html_url": f"https://gitea.example/{qualified_name}",
"updated_at": "2026-08-26T15:00:00Z",
}
self._repos[qualified_name] = repository
return self._json(201, repository, request)
if path == "repos/issues/search" and request.method == "GET":
state = str(query.get("state", ["open"])[0])
issues = [
{
**item,
"repository": {
"full_name": "acme/hub",
"name": "hub",
"owner": {"login": "acme"},
},
"updated_at": "2026-08-01T10:00:00Z",
"labels": [],
}
for item in self._issues
if state == "all" or item.get("state") == state
]
return self._paged(issues, query, request)
if path.startswith("repos/"):
path = path.removeprefix("repos/")
parts = path.split("/")
qualified_name = "/".join(parts[:2])
if len(parts) >= 2 and qualified_name not in self._repos:
return self._json(404, {"message": "not found"}, request)
if len(parts) == 2:
if request.method == "DELETE":
self._repos.pop(qualified_name, None)
self._branch_protections.pop(qualified_name, None)
self._managed_workflows.pop(qualified_name, None)
return httpx.Response(204, request=request)
if request.method != "GET":
return self._json(405, {"message": "method not allowed"}, request)
return self._json(200, self._repos[qualified_name], request, leaked_header=True)
if parts[2] == "branch_protections":
protections = self._branch_protections.setdefault(qualified_name, [])
if len(parts) == 3:
if request.method == "POST":
created = dict(body)
created.setdefault("rule_name", str(body.get("branch_name", "")))
created.setdefault("branch_name", str(body.get("rule_name", "")))
protections.append(created)
branch_name = str(created.get("rule_name") or created.get("branch_name"))
for branch in self._branches:
if branch.get("name") == branch_name:
branch["protected"] = True
return self._json(201, created, request)
return self._json(200, protections, request)
rule_name = unquote(parts[3])
protection = next(
(
item
for item in protections
if item.get("rule_name") == rule_name or item.get("branch_name") == rule_name
),
None,
)
if protection is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "PATCH":
protection.update(body)
return self._json(200, protection, request)
if request.method == "DELETE":
protections.remove(protection)
return httpx.Response(204, request=request)
return self._json(200, protection, request)
if parts[2] == "branches":
if request.method == "POST":
created = {
"name": body.get("new_branch_name", ""),
"commit": {"id": "f" * 40},
"protected": False,
}
self._branches.append(created)
return self._json(201, created, request)
if len(parts) == 4:
branch_name = unquote(parts[3])
matched_branch: dict[str, object] | None = None
for item in self._branches:
if item.get("name") == branch_name:
matched_branch = item
break
if matched_branch is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "DELETE":
self._branches.remove(matched_branch)
return httpx.Response(204, request=request)
return self._json(200, matched_branch, request)
return self._paged(self._branches, query, request)
if parts[2] == "commits" and len(parts) == 3:
return self._paged(self._commits, query, request)
if parts[2:4] == ["git", "commits"] and len(parts) == 5:
return self._json(
200,
{
**self._commits[0],
"sha": unquote(parts[4]),
"verification": {"verified": True, "reason": "gpg"},
"stats": {"additions": 42, "deletions": 2, "total": 44},
"files": [{"filename": "README.md", "status": "modified"}],
"html_url": "https://gitea.example/acme/hub/commit/example",
},
request,
)
if parts[2] == "compare" and len(parts) == 4:
compare_range = unquote(parts[3])
reverse_ranges: set[str] = set()
for pull in self._pulls:
head = pull.get("head")
base = pull.get("base")
if isinstance(head, dict) and isinstance(base, dict):
reverse_ranges.add(f"{head.get('sha', '')}...{base.get('sha', '')}")
compare_commits = (
self._reverse_compare_commits
if compare_range in reverse_ranges
else self._compare_commits
)
return self._json(
200,
{
"total_commits": len(compare_commits),
"commits": compare_commits,
"files": [{"filename": "README.md", "status": "modified"}],
},
request,
)
if parts[2] == "releases":
return self._paged(self._releases, query, request)
if parts[2] == "issues":
if request.method == "POST" and len(parts) == 3:
created_issue = {
"number": len(self._issues) + 1,
"title": body.get("title", ""),
"state": "open",
"html_url": f"https://gitea.example/acme/hub/issues/{len(self._issues) + 1}",
}
self._issues.append(created_issue)
return self._json(201, created_issue, request)
if len(parts) == 4:
issue_number = int(parts[3])
issue = next(
(item for item in self._issues if item.get("number") == issue_number), None
)
if issue is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "PATCH":
issue.update(
{
key: value
for key, value in body.items()
if key in {"title", "body", "state"}
}
)
return self._json(200, issue, request)
if request.method == "POST" and len(parts) == 5 and parts[4] == "comments":
return self._json(
201,
{
"id": 99,
"body": body.get("body", ""),
"html_url": f"https://gitea.example/acme/hub/issues/{parts[3]}#issuecomment-99",
},
request,
)
return self._paged(self._issues, query, request)
if parts[2] == "pulls" and len(parts) == 3:
if request.method == "POST":
created_pull = {
"number": len(self._pulls) + 8,
"title": body.get("title", ""),
"state": "open",
"draft": False,
"mergeable": True,
"html_url": "https://gitea.example/acme/hub/pulls/8",
"head": {"ref": body.get("head", ""), "sha": "f" * 40},
"base": {"ref": body.get("base", ""), "sha": "a" * 40},
"updated_at": "2026-08-24T20:00:00Z",
}
self._pulls.append(created_pull)
return self._json(201, created_pull, request)
return self._paged(self._pulls, query, request)
if parts[2] == "pulls" and len(parts) == 4 and parts[3] == "7":
if request.method == "PATCH":
self._pulls[0].update(
{key: value for key, value in body.items() if key in {"title", "body", "state"}}
)
if body.get("state") == "open":
self._action_run.update(
event="pull_request", status="queued", conclusion="",
head_sha=self._pulls[0]["head"]["sha"],
)
return self._json(200, self._pulls[0], request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "update"]:
if query.get("style") not in [["merge"], ["rebase"]]:
return self._json(422, {"message": "invalid style"}, request)
head = self._pulls[0].get("head")
base = self._pulls[0].get("base")
if not isinstance(head, dict) or not isinstance(base, dict):
return self._json(500, {"message": "invalid fake pull request"}, request)
head["sha"] = "d" * 40
base["sha"] = "a" * 40
return self._json(200, {}, request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "merge"]:
if self.deny_merge_transition:
return self._json(405, {"message": "merge blocked by repository policy"}, request)
strategy_key = "Do" if self.gitea_version.startswith(("1.14.", "1.25.")) else "do"
if body.get(strategy_key) not in {
"merge",
"rebase",
"rebase-merge",
"squash",
"fast-forward-only",
}:
return self._json(405, {"message": "unsupported merge strategy"}, request)
self._pulls[0].update({"state": "closed", "merged": True, "merge_commit_sha": "6" * 40})
for branch in self._branches:
if branch.get("name") == "main":
branch["commit"]["id"] = "6" * 40
return self._json(200, {}, request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "files"]:
return self._paged(self._pull_files, query, request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "reviews"]:
return self._paged(self._reviews, query, request)
if (
parts[2] == "pulls"
and len(parts) == 7
and parts[3:5] == ["7", "reviews"]
and parts[6] == "comments"
):
return self._json(200, self._review_comments, request)
if parts[2] == "commits" and len(parts) == 5 and parts[4] == "statuses":
return self._paged(self._statuses, query, request)
if parts[2:4] == ["git", "trees"] and len(parts) == 5:
if self.unsupported_search:
return self._json(501, {"message": "unsupported"}, request)
return self._json(
200,
{
"sha": "a" * 40,
"tree": [
{
"path": "README.md",
"type": "blob",
"sha": "d" * 40,
"size": len(self._file.encode()),
}
],
"truncated": False,
},
request,
)
if parts[2:4] == ["git", "blobs"] and len(parts) == 5:
return self._json(
200,
{
"sha": parts[4],
"encoding": "base64",
"content": base64.b64encode(self._file.encode()).decode(),
"size": len(self._file.encode()),
},
request,
)
if parts[2:4] == ["actions", "workflows"] and len(parts) == 4:
workflows = []
if qualified_name in self._managed_workflows:
workflows.append(
{
"id": "managed-validation.yml",
"name": "Managed validation",
"path": MANAGED_VALIDATION_PATH,
}
)
return self._json(
200,
{"workflows": workflows},
request,
)
if parts[2:4] == ["actions", "workflows"] and len(parts) == 6 and parts[5] == "dispatches":
raw_inputs = body.get("inputs")
inputs: dict[str, Any] = raw_inputs if isinstance(raw_inputs, dict) else {}
profile = str(inputs.get("profile", "full"))
ref = str(body.get("ref", ""))
dispatch_branch: dict[str, Any] | None = next(
(item for item in self._branches if item.get("name") == ref), None
)
head_sha = str(
(dispatch_branch or {}).get("commit", {}).get("id", self._action_run["head_sha"])
)
self._action_run.update(
{
"event": "workflow_dispatch",
"head_branch": ref,
"head_sha": head_sha,
"workflow_id": unquote(parts[4]),
"display_title": f"Managed validation / {profile}",
}
)
self._action_job["name"] = "full"
return self._json(200, self._action_run, request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 4:
return self._json(200, {"workflow_runs": [self._action_run]}, request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 6 and parts[5] == "rerun":
self._action_run.update(status="queued", conclusion="")
return httpx.Response(204, request=request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 5:
return self._json(200, self._action_run, request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 6 and parts[5] == "jobs":
return self._json(200, {"jobs": [self._action_job]}, request)
if parts[2:4] == ["actions", "jobs"] and len(parts) == 6 and parts[5] == "logs":
return httpx.Response(
200,
headers={"content-type": "text/plain"},
content=self._action_job_logs.encode(),
request=request,
)
if parts[2] == "contents" and len(parts) == 3:
return self._json(
200,
[
{
"name": "README.md",
"path": "README.md",
"type": "file",
"size": len(self._file.encode()),
"sha": "d" * 40,
}
],
request,
)
if (
parts[2] == "contents"
and len(parts) >= 4
and "/".join(parts[3:]) == MANAGED_VALIDATION_PATH
):
existing = self._managed_workflows.get(qualified_name)
protection = next(iter(self._branch_protections.get(qualified_name, [])), None)
temporary_push_allowed = bool(
protection
and protection.get("enable_push") is True
and protection.get("enable_push_whitelist") is True
and "acme" in protection.get("push_whitelist_usernames", [])
)
if (
request.method in {"POST", "PUT"}
and self.deny_protected_workflow_writes
and protection is not None
and not temporary_push_allowed
):
return self._json(403, {"message": "protected branch"}, request)
if request.method == "POST":
if existing is not None:
return self._json(422, {"message": "already exists"}, request)
self._managed_workflows[qualified_name] = base64.b64decode(
str(body.get("content", ""))
).decode()
return self._json(
201,
{"content": {"path": MANAGED_VALIDATION_PATH, "sha": "9" * 40}},
request,
)
if existing is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "PUT":
self._managed_workflows[qualified_name] = base64.b64decode(
str(body.get("content", ""))
).decode()
return self._json(
200,
{"content": {"path": MANAGED_VALIDATION_PATH, "sha": "9" * 40}},
request,
)
if request.method == "DELETE":
self._managed_workflows.pop(qualified_name, None)
return self._json(200, {"commit": {"sha": "7" * 40}}, request)
return self._json(
200,
{
"name": "managed-validation.yml",
"path": MANAGED_VALIDATION_PATH,
"sha": "d" * 40,
"content": base64.b64encode(existing.encode()).decode(),
"encoding": "base64",
},
request,
)
if parts[2] == "contents" and len(parts) >= 4 and "/".join(parts[3:]) == "README.md":
if request.method == "DELETE":
return self._json(200, {"commit": {"sha": "7" * 40}}, request)
if request.method in {"POST", "PUT"}:
if request.method == "POST" and self._repos[qualified_name].get("empty"):
branch_name = str(body.get("branch", "main"))
self._repos[qualified_name]["empty"] = False
self._repos[qualified_name]["default_branch"] = branch_name
if not any(item.get("name") == branch_name for item in self._branches):
self._branches.append(
{
"name": branch_name,
"commit": {"id": "8" * 40},
"protected": False,
}
)
return self._json(
201 if request.method == "POST" else 200,
{
"content": {"path": "README.md", "sha": "9" * 40},
"commit": {"sha": "8" * 40},
},
request,
)
return self._json(
200,
{
"name": "README.md",
"path": "README.md",
"sha": "d" * 40,
"content": base64.b64encode(self._file.encode()).decode(),
"encoding": "base64",
"token": "fixture-token-must-not-leak",
},
request,
)
return self._json(404, {"message": "not found"}, request)
def _paged(
self, items: list[dict[str, Any]], query: dict[str, list[str]], request: httpx.Request
) -> httpx.Response:
page = max(int(query.get("page", ["1"])[0]), 1)
limit = max(int(query.get("limit", ["100"])[0]), 1)
start = (page - 1) * limit
return self._json(200, items[start : start + limit], request)
@staticmethod
def _json(
status: int, payload: object, request: httpx.Request, *, leaked_header: bool = False
) -> httpx.Response:
headers = {"content-type": "application/json"}
if leaked_header:
headers["x-fixture-token"] = "fixture-token-must-not-leak"
return httpx.Response(
status, headers=headers, content=json.dumps(payload).encode(), request=request
)
+59
View File
@@ -0,0 +1,59 @@
"""Bounded helpers for destructive Gitea pull-request lifecycle cleanup."""
from __future__ import annotations
import re
from typing import Any, Protocol
from packages.connector_kit import GiteaError, RegisteredRepository
_VIRTUAL_PULL_HEAD = re.compile(r"refs/pull/[1-9][0-9]*/head")
class PullRequestReader(Protocol):
def pull_requests(
self, repository: RegisteredRepository, page: int, limit: int
) -> list[dict[str, Any]]: ...
def cleanup_basis(
client: PullRequestReader,
repository: RegisteredRepository,
branch: str,
expected_head_sha: str,
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
"""Return the merged/closed PR proving one exact branch is cleanup-eligible."""
related: list[dict[str, Any]] = []
for page_number in range(1, 11):
pulls = client.pull_requests(repository, page_number, 100)
for item in pulls:
head = item.get("head") or {}
head_ref = str(head.get("ref", ""))
head_sha = str(head.get("sha", ""))
if head_ref == branch or (
head_sha == expected_head_sha and _VIRTUAL_PULL_HEAD.fullmatch(head_ref)
):
related.append(item)
if len(pulls) < 100:
break
if any(str(item.get("state", "")) == "open" for item in related):
raise GiteaError(
"GITEA_TARGET_INVALID",
"branch is still referenced by an open pull request; close or merge it first",
)
merged = next((item for item in related if bool(item.get("merged", False))), None)
closed = next(
(
item
for item in related
if str(item.get("state", "")) == "closed" and not bool(item.get("merged", False))
),
None,
)
if merged is None and closed is None:
raise GiteaError(
"GITEA_TARGET_INVALID",
"only a change branch from a merged or closed pull request can be deleted",
)
return merged, closed
+153
View File
@@ -0,0 +1,153 @@
"""Stable managed-validation contract used by Gitea governance tools."""
from __future__ import annotations
import hashlib
MANAGED_VALIDATION_PATH = ".gitea/workflows/managed-validation.yml"
MANAGED_VALIDATION_CONTEXT = "Managed validation / full (pull_request)"
# Keep this workflow pull-request/dispatch only. Bulk governance must never create
# a push-triggered Actions storm across the portfolio.
MANAGED_VALIDATION_WORKFLOW = """name: Managed validation
run-name: "Managed validation / ${{ inputs.profile || 'full' }}"
on:
pull_request:
workflow_dispatch:
inputs:
profile:
description: Allowlisted validation profile
required: true
default: full
type: choice
options: [test, lint, typecheck, build, security, full]
permissions:
contents: read
concurrency:
group: managed-validation-${{ gitea.repository }}-${{ gitea.ref }}
cancel-in-progress: true
jobs:
full:
name: full
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install .NET 8 for solution repositories
if: ${{ hashFiles('*.sln') != '' }}
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: '8.0.x'
- name: Validate repository with a bounded profile
shell: bash
env:
REQUESTED_PROFILE: ${{ inputs.profile }}
run: |
set -euo pipefail
profile="${REQUESTED_PROFILE:-full}"
case "${profile}" in
test|lint|typecheck|build|security|full) ;;
*) echo "Profile is not allowlisted" >&2; exit 2 ;;
esac
python_tests_ran=0
git diff --check
if git grep -nE '^(<<<<<<< |=======$|>>>>>>> )' -- . ':!*.lock' ':!*.patch'; then
echo "Unresolved merge markers detected" >&2
exit 1
fi
if [[ -f pyproject.toml || -f requirements.txt ]]; then
# Compile only tracked Python sources. Running compileall after a
# Node install would otherwise traverse node_modules and turn a
# lightweight baseline into a large runner workload.
git ls-files -z '*.py' | xargs -0 -r python -m py_compile
if [[ -f uv.lock ]]; then
python -m venv "${RUNNER_TEMP}/managed-uv"
uv_python="${RUNNER_TEMP}/managed-uv/bin/python"
"${uv_python}" -m pip install --disable-pip-version-check uv==0.10.0
managed_uv="${RUNNER_TEMP}/managed-uv/bin/uv"
export UV_PROJECT_ENVIRONMENT="${RUNNER_TEMP}/managed-project-venv"
"${managed_uv}" sync --locked
export PATH="${UV_PROJECT_ENVIRONMENT}/bin:${PATH}"
if [[ "${profile}" == test || "${profile}" == full ]]; then
if "${managed_uv}" run python -c 'import pytest' 2>/dev/null; then
"${managed_uv}" run python -m pytest
python_tests_ran=1
fi
fi
if [[ "${profile}" == lint || "${profile}" == full ]]; then
if "${managed_uv}" run python -c 'import ruff' 2>/dev/null; then
"${managed_uv}" run python -m ruff check .
fi
fi
elif [[ -f requirements.txt ]]; then
python -m venv "${RUNNER_TEMP}/managed-python"
managed_python="${RUNNER_TEMP}/managed-python/bin/python"
"${managed_python}" -m pip install --disable-pip-version-check -r requirements.txt
export PATH="${RUNNER_TEMP}/managed-python/bin:${PATH}"
if [[ "${profile}" == test || "${profile}" == full ]]; then
if "${managed_python}" -c 'import pytest' 2>/dev/null; then
"${managed_python}" -m pytest
python_tests_ran=1
fi
fi
fi
fi
# Prepare Python before invoking Node scripts. Polyglot repositories
# commonly delegate their test script to Python and need the managed
# virtual environment to be active first.
if [[ -f package.json ]]; then
corepack enable
run_node_test=1
if [[ "${python_tests_ran}" == 1 ]] && node -e '
const script = String(require("./package.json").scripts?.test || "");
const delegated = /^\\s*python(?:3)?\\s+-m\\s+(?:pytest|unittest)\\b/.test(script);
process.exit(delegated ? 0 : 1);
'; then
run_node_test=0
fi
if [[ -f pnpm-lock.yaml ]]; then
pnpm install --frozen-lockfile
if [[ "${run_node_test}" == 1 ]]; then
[[ "${profile}" == test || "${profile}" == full ]] && pnpm --if-present test
fi
[[ "${profile}" == lint || "${profile}" == full ]] && pnpm --if-present lint
[[ "${profile}" == typecheck || "${profile}" == full ]] && pnpm --if-present typecheck
[[ "${profile}" == build || "${profile}" == full ]] && pnpm --if-present build
elif [[ -f package-lock.json ]]; then
npm ci
if [[ "${run_node_test}" == 1 ]]; then
[[ "${profile}" == test || "${profile}" == full ]] && npm run --if-present test
fi
[[ "${profile}" == lint || "${profile}" == full ]] && npm run --if-present lint
if [[ "${profile}" == typecheck || "${profile}" == full ]]; then
npm run --if-present typecheck
fi
[[ "${profile}" == build || "${profile}" == full ]] && npm run --if-present build
fi
fi
if [[ -f go.mod ]]; then
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
go test ./...
fi
fi
if [[ -f Cargo.toml ]]; then
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
cargo test --locked
fi
fi
if compgen -G '*.sln' >/dev/null; then
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
dotnet test --configuration Release
fi
fi
"""
MANAGED_VALIDATION_DIGEST = hashlib.sha256(MANAGED_VALIDATION_WORKFLOW.encode("utf-8")).hexdigest()
File diff suppressed because it is too large. Load diff
+5
View File
@@ -0,0 +1,5 @@
"""Project Knowledge connector."""
from .server import KNOWLEDGE_RESOURCES, KNOWLEDGE_TOOLS, KnowledgeConnector
__all__ = ["KNOWLEDGE_RESOURCES", "KNOWLEDGE_TOOLS", "KnowledgeConnector"]
+385
View File
@@ -0,0 +1,385 @@
"""Project Knowledge connector surface: read-only, scoped, source-backed MCP contracts."""
from __future__ import annotations
from collections.abc import Callable, Iterable
from datetime import UTC, datetime
from typing import Any
from packages.connector_kit import (
ConnectorContext,
InMemoryKnowledgeIndex,
PageRequest,
ResourcePublication,
SourceType,
ToolPublication,
decode_cursor,
encode_cursor,
)
from packages.connector_kit.knowledge import DocumentNotFound
from packages.hub_policy import Governance, governed_tool
class KnowledgeConnector:
"""Expose only registered project documents; no physical path is an input."""
connector_type = "knowledge"
version = "1.0.0"
def __init__(self, index: InMemoryKnowledgeIndex, governance: Governance | None = None) -> None:
self.index = index
self.governance = governance
def _envelope(
self,
context: ConnectorContext,
data: Any,
*,
truncated: bool = False,
next_cursor: str | None = None,
) -> dict[str, Any]:
return {
"data": data,
"meta": {
"project": context.project_slug,
"environment": context.environment,
"source": "knowledge",
"truncated": truncated,
"nextCursor": next_cursor,
"generatedAt": datetime.now(UTC).isoformat(),
},
"warnings": [],
}
@staticmethod
def _hit_payload(item: Any) -> dict[str, Any]:
return {
"uri": item.uri,
"title": item.title,
"path": item.path,
"snippet": item.snippet,
"sourceType": item.source_type,
"sourceRevision": item.source_revision,
"repositoryRef": item.repository_ref,
"repositoryCommit": item.repository_commit,
"contentHash": item.content_hash,
"modifiedAt": item.modified_at.isoformat(),
"score": item.score,
"truncated": item.truncated,
}
@governed_tool("knowledge.document.search")
def document_search(
self,
context: ConnectorContext,
query: str,
*,
source_types: Iterable[SourceType] = (),
page: PageRequest | None = None,
) -> dict[str, Any]:
request = page or PageRequest()
result = self.index.search(
context.project_id,
query,
source_types=source_types,
limit=min(request.limit, 20),
cursor=request.cursor,
)
data = {
"items": [self._hit_payload(item) for item in result.items],
"nextCursor": result.next_cursor,
"truncated": result.truncated,
}
return self._envelope(
context, data, truncated=result.truncated, next_cursor=result.next_cursor
)
@governed_tool("knowledge.document.get")
def document_get(
self, context: ConnectorContext, uri: str, *, max_bytes: int = 200_000
) -> dict[str, Any]:
if not 1 <= max_bytes <= 262_144:
raise ValueError("max_bytes is outside the connector output budget")
document = self.index.get_document(context.project_id, uri)
encoded = document.content.encode("utf-8")
truncated = len(encoded) > max_bytes
content = (
encoded[:max_bytes].decode("utf-8", errors="ignore") if truncated else document.content
)
data = {
"uri": document.uri,
"title": document.title,
"mimeType": document.mime_type,
"content": content,
"path": document.external_ref,
"sourceType": document.source_type,
"sourceRevision": document.source_revision,
"repositoryRef": document.repository_ref,
"repositoryCommit": document.repository_commit,
"contentHash": document.content_hash,
"modifiedAt": document.modified_at.isoformat(),
"truncated": truncated,
}
return self._envelope(context, data, truncated=truncated)
@governed_tool("knowledge.source.list")
def source_list(
self,
context: ConnectorContext,
*,
source_type: SourceType | None = None,
page: PageRequest | None = None,
) -> dict[str, Any]:
request = page or PageRequest(limit=25)
sources = [
source
for source in self.index.list_sources(context.project_id)
if source_type is None or source.source_type == source_type
]
cursor = decode_cursor(
request.cursor, str(context.project_id), "sources", str(source_type or "")
)
items = sources[cursor.offset : cursor.offset + min(request.limit, 100)]
offset = cursor.offset + len(items)
next_cursor = (
encode_cursor(offset, str(context.project_id), "sources", str(source_type or ""))
if offset < len(sources)
else None
)
data = {
"items": [
{
"sourceRef": item.source_ref,
"sourceType": item.source_type,
"uri": item.uri,
"status": item.status,
"sourceRevision": item.source_revision,
"lastIndexedAt": item.last_indexed_at.isoformat()
if item.last_indexed_at
else None,
"documentCount": item.document_count,
}
for item in items
],
"nextCursor": next_cursor,
"truncated": next_cursor is not None,
}
return self._envelope(
context, data, truncated=next_cursor is not None, next_cursor=next_cursor
)
@governed_tool("knowledge.index.status")
def index_status(self, context: ConnectorContext) -> dict[str, Any]:
status = self.index.status(context.project_id)
data = {
"status": status.status,
"sourceCount": status.source_count,
"documentCount": status.document_count,
"lastCompletedAt": status.last_completed_at.isoformat()
if status.last_completed_at
else None,
"lastError": status.last_error,
"generatedAt": datetime.now(UTC),
}
return self._envelope(context, data)
@governed_tool("knowledge.resource.get")
def resource_get(self, context: ConnectorContext, uri: str) -> dict[str, Any]:
if not uri.startswith("itworx-docs://"):
raise DocumentNotFound("resource URI is not registered")
return self.document_get(context, uri)
@governed_tool("knowledge.adr.list")
def adr_list(
self, context: ConnectorContext, *, page: PageRequest | None = None
) -> dict[str, Any]:
request = page or PageRequest(limit=25)
documents = self.index.documents_for_source(context.project_id, SourceType.ADR)
cursor = decode_cursor(request.cursor, str(context.project_id), "adr")
items = documents[cursor.offset : cursor.offset + min(request.limit, 100)]
offset = cursor.offset + len(items)
next_cursor = (
encode_cursor(offset, str(context.project_id), "adr")
if offset < len(documents)
else None
)
data = {
"items": [
{
"uri": doc.uri,
"number": doc.external_ref,
"title": doc.title,
"status": "indexed",
"updatedAt": doc.modified_at.isoformat(),
}
for doc in items
],
"nextCursor": next_cursor,
"truncated": next_cursor is not None,
}
return self._envelope(
context, data, truncated=next_cursor is not None, next_cursor=next_cursor
)
@governed_tool("knowledge.project.summary")
def project_summary(self, context: ConnectorContext) -> dict[str, Any]:
status = self.index.status(context.project_id)
sources = [source.uri for source in self.index.list_sources(context.project_id)]
summary = (
f"{status.document_count} indexed documents across "
f"{status.source_count} registered sources."
)
return self._envelope(
context,
{
"summary": summary,
"sources": sources,
"sourceRevision": status.last_completed_at.isoformat()
if status.last_completed_at
else "never",
"generatedAt": datetime.now(UTC),
},
)
def tools(self) -> tuple[ToolPublication, ...]:
return KNOWLEDGE_TOOLS
def resources(self) -> tuple[ResourcePublication, ...]:
return KNOWLEDGE_RESOURCES
def invoke(self, tool: str, context: ConnectorContext, **arguments: Any) -> dict[str, Any]:
methods: dict[str, Callable[..., dict[str, Any]]] = {
"knowledge.document.search": self.document_search,
"knowledge.document.get": self.document_get,
"knowledge.source.list": self.source_list,
"knowledge.index.status": self.index_status,
"knowledge.adr.list": self.adr_list,
"knowledge.project.summary": self.project_summary,
"knowledge.resource.get": self.resource_get,
}
method = methods.get(tool)
if method is None:
raise ValueError("unknown knowledge tool")
return method(context, **arguments)
def health(self) -> dict[str, Any]:
return {"status": "healthy", "component": "knowledge-connector"}
def readiness(self) -> dict[str, Any]:
return {"status": "ready", "component": "knowledge-connector"}
_COMMON_EMPTY_INPUT = {
"type": "object",
"additionalProperties": False,
"properties": {},
"required": [],
}
_COMMON_PAGE = {
"type": "object",
"additionalProperties": False,
"properties": {
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
"cursor": {"type": ["string", "null"], "maxLength": 500},
},
"required": [],
}
_COMMON_OUTPUT = {
"type": "object",
"additionalProperties": False,
"properties": {
"data": {"type": "object"},
"meta": {"type": "object"},
"warnings": {"type": "array"},
},
"required": ["data", "meta", "warnings"],
}
KNOWLEDGE_TOOLS = (
ToolPublication(
canonical_name="knowledge.document.search",
version="1.0.0",
connector_type="knowledge",
risk_class="R1",
input_schema={
"type": "object",
"additionalProperties": False,
"properties": {
"query": {"type": "string", "minLength": 1, "maxLength": 1000},
"sourceTypes": {"type": "array", "maxItems": 8},
"limit": {"type": "integer", "minimum": 1, "maximum": 20},
"cursor": {"type": ["string", "null"], "maxLength": 500},
},
"required": ["query"],
},
output_schema=_COMMON_OUTPUT,
required_scopes=("project:{project}:read",),
),
ToolPublication(
canonical_name="knowledge.document.get",
version="1.0.0",
connector_type="knowledge",
risk_class="R1",
input_schema={
"type": "object",
"additionalProperties": False,
"properties": {
"uri": {
"type": "string",
"pattern": "^itworx-docs://",
"maxLength": 2048,
}
},
"required": ["uri"],
},
output_schema=_COMMON_OUTPUT,
required_scopes=("project:{project}:read",),
),
ToolPublication(
canonical_name="knowledge.source.list",
version="1.0.0",
connector_type="knowledge",
risk_class="R1",
input_schema=_COMMON_PAGE,
output_schema=_COMMON_OUTPUT,
required_scopes=("project:{project}:read",),
),
ToolPublication(
canonical_name="knowledge.index.status",
version="1.0.0",
connector_type="knowledge",
risk_class="R1",
input_schema=_COMMON_EMPTY_INPUT,
output_schema=_COMMON_OUTPUT,
required_scopes=("project:{project}:read",),
),
ToolPublication(
canonical_name="knowledge.adr.list",
version="1.0.0",
connector_type="knowledge",
risk_class="R1",
input_schema=_COMMON_PAGE,
output_schema=_COMMON_OUTPUT,
required_scopes=("project:{project}:read",),
),
ToolPublication(
canonical_name="knowledge.project.summary",
version="1.0.0",
connector_type="knowledge",
risk_class="R1",
input_schema=_COMMON_EMPTY_INPUT,
output_schema=_COMMON_OUTPUT,
required_scopes=("project:{project}:read",),
),
)
KNOWLEDGE_RESOURCES = (
ResourcePublication(
uri_template="itworx-docs://{project}/{source}/{documentId}",
connector_type="knowledge",
risk_class="R1",
mime_types=("text/markdown", "text/plain", "application/json"),
max_bytes=262_144,
required_scopes=("project:{project}:read",),
),
)
+6
View File
@@ -0,0 +1,6 @@
"""Read-only MobilityOps (Fleet Ops) MCP connector."""
from .fake import FakeMobilityOpsServer
from .server import MOBILITYOPS_TOOLS, MobilityOpsConnector
__all__ = ["MOBILITYOPS_TOOLS", "FakeMobilityOpsServer", "MobilityOpsConnector"]
+129
View File
@@ -0,0 +1,129 @@
"""Deterministic in-memory MobilityOps HTTP transport for contract tests."""
from __future__ import annotations
import json
from urllib.parse import parse_qs, urlparse
import httpx
class FakeMobilityOpsServer:
"""A tiny MobilityOps API fixture with observable requests and no network."""
def __init__(
self,
*,
unauthorized: bool = False,
redirect_to: str | None = None,
vehicle_not_found: bool = False,
unreachable: bool = False,
) -> None:
self.unauthorized = unauthorized
self.redirect_to = redirect_to
self.vehicle_not_found = vehicle_not_found
self.unreachable = unreachable
self.recorded_methods: list[str] = []
self.recorded_service_tokens: list[str] = []
self.recorded_client_ids: list[str] = []
self.recorded_correlation_ids: list[str] = []
self.recorded_locales: list[str | None] = []
self._summary = {
"tenant": "Northstar Mobility Demo",
"metrics": {
"available": 21,
"rented": 11,
"cleaning": 6,
"maintenance": 5,
"blocked": 7,
"open_quality_issues": 3,
"pending_or_failed_workflows": 1,
},
}
self._attention = [
{
"vehicle_ref": "MO-016",
"severity": "high",
"rule_type": "vehicle_status_conflict",
"summary": "Booking overlap detected for this vehicle.",
"detected_at": "2026-08-01T09:00:00Z",
"token": "fixture-token-must-not-leak",
}
]
self._vehicle = {
"public_ref": "MO-016",
"make": "Volkswagen",
"model": "ID.4",
"model_year": 2024,
"location": "Antwerp Depot",
"operational_status": "available",
"odometer_km": 18420,
"next_service_km": 40000,
"open_quality_issue_count": 1,
"current_booking_ref": None,
}
self._answer = {
"answer": "Report the damage and route the vehicle to maintenance.",
"evidence_state": "grounded",
"sources": [
{
"document_id": "damage-procedure",
"title": "Damage handling procedure",
"version": "1",
"section": "Detection",
"excerpt": "Inspect the vehicle for visible damage.",
}
],
"provider": "demo",
"correlation_id": "fixture-correlation-id",
}
def transport(self) -> httpx.MockTransport:
return httpx.MockTransport(self._handle)
def _handle(self, request: httpx.Request) -> httpx.Response:
self.recorded_methods.append(request.method)
self.recorded_service_tokens.append(request.headers.get("x-service-token", ""))
self.recorded_client_ids.append(request.headers.get("x-client-id", ""))
self.recorded_correlation_ids.append(request.headers.get("x-correlation-id", ""))
if self.unreachable:
raise httpx.ConnectError("fixture target is unreachable", request=request)
if self.redirect_to is not None:
return httpx.Response(302, headers={"location": self.redirect_to}, request=request)
if self.unauthorized:
return self._json(401, {"code": "UNAUTHORIZED_SERVICE"}, request)
parsed = urlparse(str(request.url))
path = parsed.path.removeprefix("/api/v1/integrations/mcp")
query = parse_qs(parsed.query)
if path == "/operations-summary" and request.method == "GET":
return self._json(200, self._summary, request)
if path == "/attention-vehicles" and request.method == "GET":
limit = int(query.get("limit", ["20"])[0])
return self._json(200, self._attention[:limit], request)
if path.startswith("/vehicles/") and request.method == "GET":
if self.vehicle_not_found:
return self._json(404, {"code": "VEHICLE_NOT_FOUND"}, request)
return self._json(200, self._vehicle, request, leaked_header=True)
if path == "/search-knowledge" and request.method == "POST":
body = json.loads(request.content or b"{}")
self.recorded_locales.append(body.get("locale"))
# MobilityOps echoes the correlation id it actually received.
answer = dict(self._answer)
answer["correlation_id"] = (
request.headers.get("x-correlation-id") or answer["correlation_id"]
)
return self._json(200, answer, request)
return self._json(404, {"code": "NOT_FOUND"}, request)
@staticmethod
def _json(
status: int, payload: object, request: httpx.Request, *, leaked_header: bool = False
) -> httpx.Response:
headers = {"content-type": "application/json"}
if leaked_header:
headers["x-fixture-token"] = "fixture-token-must-not-leak"
return httpx.Response(
status, headers=headers, content=json.dumps(payload).encode(), request=request
)
+318
View File
@@ -0,0 +1,318 @@
"""Read-only MCP connector for the MobilityOps (Fleet Ops) operations API."""
from __future__ import annotations
from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any
from packages.connector_kit import (
DEFAULT_KNOWLEDGE_LOCALE,
KNOWLEDGE_LOCALES,
ConnectorContext,
MobilityOpsClient,
MobilityOpsError,
ResourcePublication,
ToolPublication,
safe_snippet,
)
from packages.hub_policy import Governance, governed_tool
class MobilityOpsConnector:
"""Expose MobilityOps's four read-only operational tools.
Unlike Gitea, there is exactly one configured MobilityOps target per
environment -- no per-project repository registry, every call is
authorized purely by the caller's project/environment context via the
governance layer, matching MobilityOps's own narrow read-only API surface
(`docs/10-mcp-hub-integration.md` in the MobilityOps repository).
"""
connector_type = "mobilityops"
version = "1.0.0"
#: Identifies readiness probes in MobilityOps's own service-request audit log.
READINESS_CLIENT_ID = "itworx-mcp-hub:readiness"
def __init__(self, client: MobilityOpsClient, governance: Governance | None = None) -> None:
self.client = client
self.governance = governance
def _envelope(
self,
context: ConnectorContext,
data: Any,
*,
source: str = "mobilityops",
truncated: bool = False,
warnings: tuple[str, ...] = (),
) -> dict[str, Any]:
return {
"data": data,
"meta": {
"project": context.project_slug,
"environment": context.environment,
"source": source,
"truncated": truncated,
"nextCursor": None,
"generatedAt": datetime.now(UTC).isoformat(),
},
"warnings": list(warnings),
}
@staticmethod
def _client_id(context: ConnectorContext) -> str:
return f"itworx-mcp-hub:{context.project_slug}:{context.client_id or 'anonymous'}"
@staticmethod
def _optional_text(value: Any, limit: int) -> str | None:
"""Pass through exactly what MobilityOps returned, or nothing at all.
Never substitutes a placeholder: an absent `provider`/`correlationId` is
reported as `null` rather than invented.
"""
if value is None:
return None
text = str(value).strip()
return text[:limit] if text else None
@governed_tool("mobilityops.operations.summary")
def operations_summary(self, context: ConnectorContext) -> dict[str, Any]:
payload = self.client.operations_summary(
self._client_id(context), correlation_id=context.correlation_id
)
raw_metrics = payload.get("metrics")
metrics: dict[str, Any] = raw_metrics if isinstance(raw_metrics, dict) else {}
data = {
"tenant": safe_snippet(str(payload.get("tenant", "")), max_bytes=200),
"metrics": {
"available": int(metrics.get("available", 0)),
"rented": int(metrics.get("rented", 0)),
"cleaning": int(metrics.get("cleaning", 0)),
"maintenance": int(metrics.get("maintenance", 0)),
"blocked": int(metrics.get("blocked", 0)),
"openQualityIssues": int(metrics.get("open_quality_issues", 0)),
"pendingOrFailedWorkflows": int(metrics.get("pending_or_failed_workflows", 0)),
},
}
return self._envelope(context, data)
@governed_tool("mobilityops.attention.list")
def attention_list(
self,
context: ConnectorContext,
*,
minimum_severity: str = "medium",
date: str | None = None,
limit: int = 20,
) -> dict[str, Any]:
if minimum_severity not in {"low", "medium", "high"}:
raise ValueError("minimum_severity must be one of low, medium, high")
if not 1 <= limit <= 50:
raise ValueError("limit must be between 1 and 50")
items = self.client.attention_vehicles(
self._client_id(context),
minimum_severity=minimum_severity,
date=date,
limit=limit,
correlation_id=context.correlation_id,
)
mapped = [
{
"vehicleRef": str(item.get("vehicle_ref", ""))[:64],
"severity": str(item.get("severity", ""))[:32],
"ruleType": str(item.get("rule_type", ""))[:120],
"summary": safe_snippet(str(item.get("summary", "")), max_bytes=1000),
"detectedAt": str(item.get("detected_at", ""))[:64],
}
for item in items
]
data = {"items": mapped, "nextCursor": None, "truncated": False}
return self._envelope(context, data, truncated=False)
@governed_tool("mobilityops.vehicle.get")
def vehicle_get(self, context: ConnectorContext, vehicle_ref: str) -> dict[str, Any]:
payload = self.client.vehicle_details(
self._client_id(context), vehicle_ref, correlation_id=context.correlation_id
)
data = {
"publicRef": str(payload.get("public_ref", ""))[:64],
"make": str(payload.get("make", ""))[:120],
"model": str(payload.get("model", ""))[:120],
"modelYear": int(payload.get("model_year", 0)),
"location": str(payload.get("location", ""))[:200],
"operationalStatus": str(payload.get("operational_status", ""))[:64],
"odometerKm": int(payload.get("odometer_km", 0)),
"nextServiceKm": int(payload.get("next_service_km", 0)),
"openQualityIssueCount": int(payload.get("open_quality_issue_count", 0)),
"currentBookingRef": (
str(payload["current_booking_ref"])[:64]
if payload.get("current_booking_ref")
else None
),
}
return self._envelope(context, data, source=f"mobilityops:{vehicle_ref}")
@governed_tool("mobilityops.knowledge.search")
def knowledge_search(
self,
context: ConnectorContext,
question: str,
*,
max_sources: int = 4,
locale: str = DEFAULT_KNOWLEDGE_LOCALE,
) -> dict[str, Any]:
if not 3 <= len(question) <= 1000:
raise ValueError("question is outside the connector budget")
if not 1 <= max_sources <= 8:
raise ValueError("max_sources must be between 1 and 8")
if locale not in KNOWLEDGE_LOCALES:
raise ValueError("locale must be one of nl-BE, en-GB, fr-BE")
payload = self.client.search_knowledge(
self._client_id(context),
question,
max_sources=max_sources,
locale=locale,
correlation_id=context.correlation_id,
)
raw_sources = payload.get("sources")
sources: list[Any] = raw_sources if isinstance(raw_sources, list) else []
data = {
"answer": safe_snippet(str(payload.get("answer", "")), max_bytes=4000),
"evidenceState": str(payload.get("evidence_state", "unavailable"))[:32],
"provider": self._optional_text(payload.get("provider"), 120),
"correlationId": self._optional_text(payload.get("correlation_id"), 128),
"sources": [
{
"documentId": str(source.get("document_id", ""))[:200],
"title": str(source.get("title", ""))[:300],
"version": str(source.get("version", ""))[:120],
"section": str(source.get("section", ""))[:300],
"excerpt": safe_snippet(str(source.get("excerpt", "")), max_bytes=1500),
}
for source in sources
if isinstance(source, dict)
],
}
return self._envelope(context, data, source="mobilityops:knowledge")
def tools(self) -> tuple[ToolPublication, ...]:
return MOBILITYOPS_TOOLS
def resources(self) -> tuple[ResourcePublication, ...]:
return ()
def invoke(self, tool: str, context: ConnectorContext, **arguments: Any) -> dict[str, Any]:
methods: dict[str, Callable[..., dict[str, Any]]] = {
"mobilityops.operations.summary": self.operations_summary,
"mobilityops.attention.list": self.attention_list,
"mobilityops.vehicle.get": self.vehicle_get,
"mobilityops.knowledge.search": self.knowledge_search,
}
method = methods.get(tool)
if method is None:
raise ValueError("unknown mobilityops tool")
return method(context, **arguments)
def health(self) -> dict[str, Any]:
"""Liveness only: this process answers, independent of the MobilityOps upstream."""
return {"status": "healthy", "component": "mobilityops-connector", "readOnly": True}
#: Upstream faults that mean MobilityOps answered but this connector cannot use it.
_DEGRADED_CODES = frozenset(
{
"MOBILITYOPS_ACCESS_DENIED",
"MOBILITYOPS_REQUEST_INVALID",
"MOBILITYOPS_INVALID_RESPONSE",
"MOBILITYOPS_RESPONSE_TOO_LARGE",
"MOBILITYOPS_REDIRECT_DENIED",
"MOBILITYOPS_SECRET_UNAVAILABLE",
"MOBILITYOPS_SECRET_INVALID",
"MOBILITYOPS_SECRET_REFERENCE_INVALID",
}
)
def readiness(self) -> dict[str, Any]:
"""Ready only when the real MobilityOps upstream actually answers.
Uses one short, bounded, authenticated upstream read. Only the connector's own
stable error code is reported -- never a raw exception, response body, header or
secret.
"""
snapshot: dict[str, Any] = {
"component": "mobilityops-connector",
"readOnly": True,
"upstream": "mobilityops",
}
try:
self.client.readiness_probe(self.READINESS_CLIENT_ID)
except MobilityOpsError as exc:
snapshot["status"] = "degraded" if exc.code in self._DEGRADED_CODES else "unavailable"
snapshot["reason"] = exc.code
return snapshot
except Exception: # noqa: BLE001 - readiness must never surface a raw exception.
snapshot["status"] = "unavailable"
snapshot["reason"] = "MOBILITYOPS_READINESS_FAILED"
return snapshot
snapshot["status"] = "ready"
return snapshot
_EMPTY_INPUT = {"type": "object", "additionalProperties": False, "properties": {}}
_ENVELOPE_OUTPUT = {"type": "object", "additionalProperties": False}
_OPERATIONS_SUMMARY_INPUT = _EMPTY_INPUT
_ATTENTION_LIST_INPUT = {
"type": "object",
"additionalProperties": False,
"properties": {
"minimum_severity": {"type": "string", "enum": ["low", "medium", "high"]},
"date": {"type": ["string", "null"], "format": "date"},
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
},
}
_VEHICLE_GET_INPUT = {
"type": "object",
"additionalProperties": False,
"properties": {"vehicleRef": {"type": "string", "minLength": 1, "maxLength": 64}},
"required": ["vehicleRef"],
}
_KNOWLEDGE_SEARCH_INPUT = {
"type": "object",
"additionalProperties": False,
"properties": {
"question": {"type": "string", "minLength": 3, "maxLength": 1000},
"max_sources": {"type": "integer", "minimum": 1, "maximum": 8},
"locale": {
"type": "string",
"enum": list(KNOWLEDGE_LOCALES),
"default": DEFAULT_KNOWLEDGE_LOCALE,
},
},
"required": ["question"],
}
_INPUT_SCHEMAS: dict[str, dict[str, Any]] = {
"mobilityops.operations.summary": _OPERATIONS_SUMMARY_INPUT,
"mobilityops.attention.list": _ATTENTION_LIST_INPUT,
"mobilityops.vehicle.get": _VEHICLE_GET_INPUT,
"mobilityops.knowledge.search": _KNOWLEDGE_SEARCH_INPUT,
}
MOBILITYOPS_TOOLS = tuple(
ToolPublication(
canonical_name=name,
version="1.0.0",
connector_type="mobilityops",
risk_class="R1",
input_schema=_INPUT_SCHEMAS[name],
output_schema=_ENVELOPE_OUTPUT,
required_scopes=("project:{project}:read",),
)
for name in (
"mobilityops.operations.summary",
"mobilityops.attention.list",
"mobilityops.vehicle.get",
"mobilityops.knowledge.search",
)
)
+12
View File
@@ -0,0 +1,12 @@
"""Read-only Unraid connector backed exclusively by the Docker read broker."""
from .fake import FakeBrokerClient
from .server import UNRAID_RESOURCES, UNRAID_TOOLS, RegisteredContainer, UnraidConnector
__all__ = [
"FakeBrokerClient",
"RegisteredContainer",
"UNRAID_RESOURCES",
"UNRAID_TOOLS",
"UnraidConnector",
]
+131
View File
@@ -0,0 +1,131 @@
"""Typed HTTP client for the internal Docker read broker."""
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Protocol
import httpx
from packages.connector_kit import ConnectorContext
from packages.hub_runtime import issue_broker_service_token
class BrokerClientError(RuntimeError):
def __init__(self, code: str, message: str, status: int = 502) -> None:
self.code = code
self.status = status
super().__init__(message)
class BrokerTokenProvider(Protocol):
def get_token(self, context: ConnectorContext) -> str: ...
class FileBrokerTokenProvider:
def __init__(self, path: str = "/run/secrets/docker_broker_service_token") -> None:
self.path = Path(path)
def get_token(self, context: ConnectorContext) -> str:
try:
secret = self.path.read_text(encoding="utf-8").strip()
except OSError as exc:
raise BrokerClientError(
"BROKER_TOKEN_UNAVAILABLE", "broker token is unavailable", 503
) from exc
if len(secret) < 32:
raise BrokerClientError("BROKER_TOKEN_INVALID", "broker signing key is invalid", 503)
return issue_broker_service_token(
secret,
context.project_slug,
context.environment,
ttl_seconds=60,
)
class HttpBrokerClient:
"""Only named broker operations exist; arbitrary paths are not accepted."""
def __init__(
self,
base_url: str,
token_provider: BrokerTokenProvider,
*,
transport: httpx.BaseTransport | None = None,
) -> None:
if not base_url.startswith("http://") or any(char in base_url for char in "?#"):
raise ValueError("broker base URL must be an internal HTTP origin without query data")
self._client = httpx.Client(base_url=base_url.rstrip("/"), timeout=10, transport=transport)
self._token_provider = token_provider
def close(self) -> None:
self._client.close()
def _get(
self, context: ConnectorContext, path: str, params: Mapping[str, str | int] | None = None
) -> dict[str, Any]:
token = self._token_provider.get_token(context)
try:
response = self._client.get(
path, params=params, headers={"Authorization": f"Bearer {token}"}
)
except (httpx.TimeoutException, httpx.NetworkError) as exc:
raise BrokerClientError(
"BROKER_UNAVAILABLE", "Docker read broker is unavailable", 503
) from exc
if response.status_code >= 400:
try:
detail = response.json()
except ValueError:
detail = {}
raise BrokerClientError(
str(detail.get("code", "BROKER_REQUEST_FAILED")),
"Docker read broker denied the request",
response.status_code,
)
try:
value = response.json()
except ValueError as exc:
raise BrokerClientError(
"BROKER_INVALID_RESPONSE", "Docker read broker returned invalid JSON"
) from exc
if not isinstance(value, dict):
raise BrokerClientError(
"BROKER_INVALID_RESPONSE", "Docker read broker returned an unexpected response"
)
return value
def host(self, context: ConnectorContext) -> dict[str, Any]:
return self._get(context, "/v1/host")
def storage(self, context: ConnectorContext) -> dict[str, Any]:
return self._get(context, "/v1/storage")
def containers(self, context: ConnectorContext, state: str | None = None) -> dict[str, Any]:
return self._get(context, "/v1/containers", {"state": state} if state else None)
def inspect(self, context: ConnectorContext, registered_id: str) -> dict[str, Any]:
return self._get(context, f"/v1/containers/{registered_id}")
def health(self, context: ConnectorContext, registered_id: str) -> dict[str, Any]:
return self._get(context, f"/v1/containers/{registered_id}/health")
def logs(
self,
context: ConnectorContext,
registered_id: str,
*,
tail: int,
since_seconds: int | None,
contains: str | None,
) -> dict[str, Any]:
params: dict[str, str | int] = {"tail": tail}
if since_seconds is not None:
params["sinceSeconds"] = since_seconds
if contains:
params["contains"] = contains
return self._get(context, f"/v1/containers/{registered_id}/logs", params)
def networks(self, context: ConnectorContext) -> dict[str, Any]:
return self._get(context, "/v1/networks")
+95
View File
@@ -0,0 +1,95 @@
"""Deterministic broker client fake for Unraid connector tests."""
from __future__ import annotations
from typing import Any
from packages.connector_kit import ConnectorContext
class FakeBrokerClient:
def __init__(self, *, fail: bool = False) -> None:
self.fail = fail
self.calls: list[str] = []
def _check(self, operation: str) -> None:
self.calls.append(operation)
if self.fail:
raise RuntimeError("broker unavailable")
def host(self, context: ConnectorContext) -> dict[str, Any]:
self._check("host")
return {
"hostRef": "unraid",
"hostname": "unraid-test",
"uptimeSeconds": 3600,
"cpuPercent": 12.5,
"memoryUsedBytes": 100,
"memoryTotalBytes": 1000,
"dockerVersion": "27.0.0",
}
def storage(self, context: ConnectorContext) -> dict[str, Any]:
self._check("storage")
return {
"items": [
{"name": "appdata", "driver": "local", "usageBytes": 1024, "capacityBytes": 4096}
]
}
def containers(self, context: ConnectorContext, state: str | None = None) -> dict[str, Any]:
self._check("containers")
return {
"items": [
{
"containerRef": "ludarium-web",
"name": "ludarium-web",
"image": "web:1",
"state": state or "running",
"health": "healthy",
"startedAt": "2026-08-01T10:00:00Z",
"restartCount": 2,
"ports": [],
"networks": ["hub-connectors"],
"labels": {"com.docker.compose.project": "ludarium"},
}
]
}
def inspect(self, context: ConnectorContext, registered_id: str) -> dict[str, Any]:
self._check("inspect")
return dict(self.containers(context)["items"][0]) | {"containerRef": registered_id}
def health(self, context: ConnectorContext, registered_id: str) -> dict[str, Any]:
self._check("health")
return {"status": "healthy", "running": True, "restartCount": 2}
def logs(
self,
context: ConnectorContext,
registered_id: str,
*,
tail: int,
since_seconds: int | None,
contains: str | None,
) -> dict[str, Any]:
self._check("logs")
return {
"containerRef": registered_id,
"lines": ["ready"],
"truncated": False,
"redactionsApplied": 0,
}
def networks(self, context: ConnectorContext) -> dict[str, Any]:
self._check("networks")
return {
"items": [
{
"name": "hub-connectors",
"networkRef": "network-1",
"driver": "bridge",
"scope": "local",
}
]
}
+284
View File
@@ -0,0 +1,284 @@
"""Project-scoped Unraid MCP connector; no Docker socket dependency."""
from __future__ import annotations
from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
from packages.connector_kit import (
ConnectorContext,
PageRequest,
ResourcePublication,
ToolPublication,
decode_cursor,
encode_cursor,
)
from packages.hub_policy import Governance, governed_tool
from .client import BrokerClientError
class RegisteredContainer:
def __init__(self, registered_id: str, project_id: UUID, environment: str) -> None:
self.registered_id = registered_id
self.project_id = project_id
self.environment = environment
class UnraidConnector:
connector_type = "unraid"
version = "1.0.0"
def __init__(
self,
client: Any,
registrations: tuple[RegisteredContainer, ...],
governance: Governance | None = None,
) -> None:
self.client = client
self.governance = governance
self._registrations = {item.registered_id: item for item in registrations}
def _resolve(self, context: ConnectorContext, registered_id: str) -> RegisteredContainer:
item = self._registrations.get(registered_id)
if (
item is None
or item.project_id != context.project_id
or item.environment != context.environment
):
raise BrokerClientError(
"UNRAID_CONTAINER_NOT_REGISTERED",
"container is not registered for this project",
403,
)
return item
@staticmethod
def _envelope(
context: ConnectorContext,
data: Any,
*,
truncated: bool = False,
next_cursor: str | None = None,
) -> dict[str, Any]:
return {
"data": data,
"meta": {
"project": context.project_slug,
"environment": context.environment,
"source": "unraid:docker-read-broker",
"truncated": truncated,
"nextCursor": next_cursor,
"generatedAt": datetime.now(UTC).isoformat(),
},
"warnings": [],
}
@governed_tool("unraid.host.status")
def host_status(self, context: ConnectorContext) -> dict[str, Any]:
return self._envelope(context, {"hostRef": "unraid", **self.client.host(context)})
@governed_tool("unraid.storage.list")
def storage_list(
self, context: ConnectorContext, *, page: PageRequest | None = None
) -> dict[str, Any]:
return self._page_items(
context, self.client.storage(context).get("items", []), page, "storage"
)
@governed_tool("unraid.container.list")
def container_list(
self,
context: ConnectorContext,
*,
state: str | None = None,
page: PageRequest | None = None,
) -> dict[str, Any]:
result = self.client.containers(context, state)
return self._page_items(context, result.get("items", []), page, "containers")
@governed_tool("unraid.container.inspect")
def container_inspect(self, context: ConnectorContext, container_ref: str) -> dict[str, Any]:
self._resolve(context, container_ref)
return self._envelope(context, self.client.inspect(context, container_ref))
@governed_tool("unraid.container.health")
def container_health(self, context: ConnectorContext, container_ref: str) -> dict[str, Any]:
self._resolve(context, container_ref)
return self._envelope(context, self.client.health(context, container_ref))
@governed_tool("unraid.container.logs")
def container_logs(
self,
context: ConnectorContext,
container_ref: str,
*,
tail: int = 200,
since_seconds: int | None = None,
contains: str | None = None,
) -> dict[str, Any]:
self._resolve(context, container_ref)
if (
not 1 <= tail <= 500
or (since_seconds is not None and not 1 <= since_seconds <= 86400)
or (contains is not None and len(contains) > 200)
):
raise ValueError("log request is outside the connector budget")
return self._envelope(
context,
self.client.logs(
context, container_ref, tail=tail, since_seconds=since_seconds, contains=contains
),
)
@governed_tool("unraid.network.list")
def network_list(
self, context: ConnectorContext, *, page: PageRequest | None = None
) -> dict[str, Any]:
return self._page_items(
context, self.client.networks(context).get("items", []), page, "networks"
)
@governed_tool("unraid.project.summary")
def project_summary(self, context: ConnectorContext) -> dict[str, Any]:
expected = sorted(
item.registered_id
for item in self._registrations.values()
if item.project_id == context.project_id and item.environment == context.environment
)
raw = self.client.containers(context, None).get("items", [])
values = raw if isinstance(raw, list) else []
containers = [
item
for item in values
if isinstance(item, dict) and str(item.get("containerRef", "")) in expected
]
observed = {str(item.get("containerRef", "")) for item in containers}
running = sum(item.get("state") == "running" for item in containers)
healthy = sum(item.get("health") == "healthy" for item in containers)
degraded = [
str(item.get("containerRef", ""))
for item in containers
if item.get("state") != "running" or item.get("health") not in {"healthy", "none"}
]
missing = [item for item in expected if item not in observed]
return self._envelope(
context,
{
"status": "healthy" if not degraded and not missing else "degraded",
"expectedContainers": len(expected),
"observedContainers": len(containers),
"runningContainers": running,
"healthyContainers": healthy,
"missingContainerRefs": missing,
"degradedContainerRefs": degraded,
"containers": containers,
},
)
@governed_tool("unraid.resource.get")
def resource_get(self, context: ConnectorContext, container_ref: str) -> dict[str, Any]:
return self.container_inspect(context, container_ref)
def _page_items(
self, context: ConnectorContext, items: Any, page: PageRequest | None, kind: str
) -> dict[str, Any]:
request = page or PageRequest(limit=25)
values = list(items) if isinstance(items, list) else []
cursor = decode_cursor(request.cursor, str(context.project_id), kind, str(request.limit))
selected = values[cursor.offset : cursor.offset + request.limit]
next_cursor = None
if cursor.offset + len(selected) < len(values):
next_cursor = encode_cursor(
cursor.offset + len(selected), str(context.project_id), kind, str(request.limit)
)
data = {"items": selected, "nextCursor": next_cursor, "truncated": next_cursor is not None}
return self._envelope(
context, data, truncated=next_cursor is not None, next_cursor=next_cursor
)
def tools(self) -> tuple[ToolPublication, ...]:
return UNRAID_TOOLS
def resources(self) -> tuple[ResourcePublication, ...]:
return UNRAID_RESOURCES
def invoke(self, tool: str, context: ConnectorContext, **arguments: Any) -> dict[str, Any]:
methods: dict[str, Callable[..., dict[str, Any]]] = {
"unraid.host.status": self.host_status,
"unraid.storage.list": self.storage_list,
"unraid.container.list": self.container_list,
"unraid.container.inspect": self.container_inspect,
"unraid.container.health": self.container_health,
"unraid.container.logs": self.container_logs,
"unraid.network.list": self.network_list,
"unraid.project.summary": self.project_summary,
}
method = methods.get(tool)
if method is None:
raise ValueError("unknown unraid tool")
return method(context, **arguments)
def health(self) -> dict[str, Any]:
return {"status": "healthy", "component": "unraid-connector", "broker": "required"}
def readiness(self) -> dict[str, Any]:
return {"status": "ready", "component": "unraid-connector", "broker": "required"}
_PAGE_INPUT = {
"type": "object",
"additionalProperties": False,
"properties": {
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
"cursor": {"type": ["string", "null"], "maxLength": 500},
},
"required": [],
}
_CONTAINER_INPUT = {
"type": "object",
"additionalProperties": False,
"properties": {"containerRef": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,127}$"}},
"required": ["containerRef"],
}
UNRAID_TOOLS = tuple(
ToolPublication(
canonical_name=name,
version="1.0.0",
connector_type="unraid",
risk_class="R1",
input_schema=input_schema,
output_schema={"type": "object"},
required_scopes=("project:{project}:read",),
)
for name, input_schema in (
(
"unraid.host.status",
{"type": "object", "additionalProperties": False, "properties": {}, "required": []},
),
(
"unraid.project.summary",
{"type": "object", "additionalProperties": False, "properties": {}, "required": []},
),
("unraid.storage.list", _PAGE_INPUT),
("unraid.container.list", _PAGE_INPUT),
("unraid.container.inspect", _CONTAINER_INPUT),
("unraid.container.health", _CONTAINER_INPUT),
("unraid.container.logs", _CONTAINER_INPUT),
("unraid.network.list", _PAGE_INPUT),
)
)
UNRAID_RESOURCES = (
ResourcePublication(
uri_template="itworx-unraid://{project}/{environment}/container/{containerRef}",
connector_type="unraid",
risk_class="R1",
mime_types=("application/json",),
max_bytes=131072,
required_scopes=("project:{project}:read",),
),
)