Skip to content

External integrations (operations notes)

Several OrpycaMCP capabilities are implemented with a pluggable provider and a functional default stub, so that the system operates end-to-end without heavy external dependencies. This guide indicates, for each one, where to plug in the real component when the environment is available (certificates, WORM storage, libraries, models), which contract stays stable, and the activation steps.

Common principle: the contract (function signatures / REST endpoints) does not change when swapping the stub for the real provider. Only the adapter is implemented and the configuration is adjusted.


1. Qualified digital signature (PKI / XAdES) — signature-service

Current state Native electronic signature (SHA-256 + identity + seal). Detects alteration; it is not a qualified digital signature.
Swap point services/signature-service/app/services/signer.py (firmar / verificar functions).
Selector SIGNER_PROVIDER (config; nativa by default).
Regulation Decreto 2364/2012 (Colombian electronic signature law) → certified for procedures that require a digital signature.

To enable PKI: 1. Add a signing library (e.g. pyhanko/endesive + cryptography) to services/signature-service/requirements.txt. 2. Implement an xades provider in signer.py with the same interface: firmar(payload, firmante) -> {payload_sha256, firma_blob, provider} and verificar(...), using an X.509 certificate, a TSA (RFC 3161) timestamp and, if applicable, CRL/OCSP validation. 3. Provision the certificate and the TSA endpoint (environment variables/secrets), select it with SIGNER_PROVIDER=xades. 4. The index signing at closure (archive → signature-service /sign-xml) and /verify already consume this contract: they switch to a digital signature with no changes in archive.


2. WORM / immutability (MinIO Object Lock) — storage-service

Current state Preservation plan + PREMIS events + AIP/BagIt. The WORM event is recorded, but there is no real Object Lock.
Swap point storage-service (MinIO client + preservation bucket; when persisting the AIP).
Regulation Acuerdo AGN 001/2024 (Colombian National Archive agreement) Art. 4.3.2.6; Decreto 2609/2012 Art. 26.

To enable WORM: 1. Create a preservation bucket with versioning and Object Lock enabled (compliance mode). 2. When packaging the AIP — Archival Information Package — (POST /api/v1/preservacion/aip), upload the object with a retention until derived from the TRD (E04, archivo_central_years) and, if applicable, a legal hold. 3. Record the WORM event (already supported) with the applied retention date. 4. Requires MinIO/S3 with Object Lock; the retention calculation already exists (E04 GET /trd/{id}/retention).


3. PDF/A validation (veraPDF) — storage-service

Current state E07 delivers best-effort PDF/A without validation. Preservation does not validate conformance.
Swap point storage-service (preservation ingestion/normalization step).
Regulation Decreto 2609/2012 Art. 32 (open standards); ISO 19005 (PDF/A).

To enable veraPDF: 1. Have veraPDF (Java CLI) or an accessible validator microservice available. 2. During normalization to PDF/A-1b/2b, invoke veraPDF and record the result as a VALIDACION_PDFA event (ok/fallo) in preservation (already supported). 3. Reject/flag non-conforming objects before packaging the AIP.


4. MCP layer — MCP server — mcp-server

Current state Complete MCP server (official mcp SDK) over stdio and HTTP streamable: exposes tools (catalog), resources (orfeo://…) and prompts, with per-session OAuth (obtains/refreshes the Keycloak token via auth-service). Open refinement: proactive refresh scheduling.
Implementation services/mcp-server/app/mcp_app.py (tools) + app/mcp_resources.py (resources+prompts) + app/mcp_stdio.py (stdio entrypoint) + the /mcp endpoint on the FastAPI app (app/main.py, StreamableHTTPSessionManager). The catalog.py + dispatcher.py core is reused unchanged.
Regulation/decision Constitution §1; ADR-019.

stdio transport (local desktop-style client): the client launches the process and talks over stdio; the session is configured via environment. There are three token modes (precedence top to bottom):

GATEWAY_URL=http://localhost:19080 \
MCP_TENANT_SLUG=icetex \
MCP_USER_PERMISSIONS=RADI_CREAR,... \
# (a) static token (no refresh):   MCP_BEARER_TOKEN=<jwt>
# (b) per-session refresh:         MCP_REFRESH_TOKEN=<refresh-token>
# (c) ROPC (username/password):    MCP_AUTH_USERNAME=ana MCP_AUTH_PASSWORD=...
python -m app.mcp_stdio

With (b)/(c) the mcp-server obtains and refreshes the Keycloak access token via auth-service (/api/v1/auth/token and /api/v1/auth/refresh, public in the gateway) and caches it until expiry.

HTTP streamable transport (remote clients): the client connects to the POST http://mcp-server:8009/mcp endpoint; the session context arrives via per-request headers: Authorization: Bearer <jwt>, X-Tenant-Slug, X-User-Permissions, X-User-Roles.

In both cases the server propagates Authorization + X-Tenant-Slug to the gateway (which resolves authentication/RBAC/tenant) and filters the visible tools by the user's permissions (ROOT sees all). Write operations remain subject to the MCP client's tool approval (human-in-the-loop) plus the permission scope.

Resources (orfeo://normativa, templates orfeo://radicado/{id}, orfeo://expediente/{id}, orfeo://expediente/{id}/indice) and prompts (radicar_pqrsd, resumen_expediente, buscar_antecedentes) are available on both transports.

The MCP layer is thus complete (tools + resources + prompts + both transports + per-session OAuth).


5. Knowledge layer — real embeddings/LLM — knowledge-service

Current state Deterministic stub embedding (hash→vector dim 64). pgvector + retrieval with ACL pre-filtering already work.
Swap point services/knowledge-service/app/services/embeddings.py (embed).
Selector EMBEDDING_PROVIDER / EMBEDDING_DIM (config).
Decision ADR-006 (configurable AI provider, local by default).

To enable real embeddings/RAG: 1. Connect an embeddings model (e.g. local sentence-transformers or an API) by implementing embed(text) -> list[float]; adjust EMBEDDING_DIM and the vector(N) column (migration) to the model's dimension. 2. For grounded RAG with citations and TRD/dependencia suggestions (advisory), connect a pluggable LLM (ADR-006); keep the ACL pre-filtering (acl @>) before retrieval. 3. It is an advisory/derived and per-tenant opt-in layer: it is not a source of truth.


6. Real postal operator (4-72 / Servientrega) — document-service

Current state Stub provider (deterministic guía, no real API).
Swap point services/document-service/app/services/postal_provider.py (generar_guia).

To enable the real operator: implement generar_guia(operador, radicado_id) (and, if applicable, the status query) against the operator's API with its credentials; the delivery status is already updated via POST /api/v1/envios/{id}/estado (callback/polling).


7. Migration from legacy Orfeo — out of scope for this repository

Scope decision: this codebase does not carry, and will not carry, data-migration tooling for existing Orfeo installations. That work is done externally, outside the repository.

Why

Every institution configured its Orfeo differently: 14- or 15-digit registration numbers, metadata in dynamic tables, its own password scheme, different database engines. A generic extractor does not exist; what exists is per-installation mapping work. Putting it here would mean maintaining a set of special cases in the core that help nobody else and age with every client.

What this repository does provide, and it is enough

The way in is the interoperable importer, which is generic and documented:

  • POST /api/v1/import in document-service — takes a ZIP package with manifiesto.json, esquema/radicado.schema.json (JSON Schema draft 2020-12 published inside the package itself), the records as JSON and their attachments.
  • All-or-nothing validation: if a schema or a SHA-256 does not match, no partial state is left in the database.
  • Identity preserved: the original registration number is not regenerated (AGN Agreement 060/2001, Law 594/2000 art. 19). A collision is skipped, never overwritten.
  • Idempotent by number, which allows repeated incremental loads.

In short: the input contract is stable and public. What gets built outside is the translator from the source system into that format.

Practical consequences

  • Orfeo-specific ETL, per-client adapters and legacy-database extraction scripts are not accepted in this repository.
  • The analysis material from real installations lives in the private documentos/planesMigracion/ repository and is business-rule reference, not migration code.
  • If a migration hits a limitation of the generic importer — say the accepted registration-number format, or reconciling the numbering sequence after a load — that is work for this repository, because it affects the input contract and benefits any source, not only Orfeo.

Summary

Capability Service Swap Needs
PKI digital signature signature-service signer.py X.509 cert + TSA + lib
WORM (Object Lock) storage-service MinIO client MinIO with Object Lock
PDF/A storage-service normalization veraPDF
MCP protocol (tools+resources+prompts, stdio+HTTP, OAuth) ✅ mcp-server done (mcp_app.py+mcp_resources.py+oauth.py+/mcp)
Embeddings/RAG knowledge-service embeddings.py model/LLM
Postal operator document-service postal_provider.py API credentials
Migration from Orfeo out of scope external translator into POST /api/v1/import

All integrations preserve their contract; enabling each one means implementing the adapter and providing the dependency/credential — with no changes to the domain logic.

Migration is the exception, and that is why it has no service: it is not an adapter that plugs in here, but work done outside against a stable input contract (§7).