"""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",), ), )