mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
M0 e M1 revisados e fixes implementados
This commit is contained in:
111
RI/services/example_mcp_servers/finance_domain/server.py
Normal file
111
RI/services/example_mcp_servers/finance_domain/server.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Example Finance-domain MCP server (EP-06-T03).
|
||||
|
||||
invoice.get (R1, read-only), payment.create (R2, side-effecting, not yet
|
||||
executed), payment.execute (R3, high-impact -- REQUIRE_APPROVAL above
|
||||
Finance.Payments' `restrictions.approval_threshold`, enforced entirely by
|
||||
config/policies/transaction.rego before the call ever reaches here). Backed
|
||||
by static mock data -- this server is deliberately "dumb" (ADR-023): it never
|
||||
authorizes anything, only the Gateway/PDP does that, upstream of this process.
|
||||
|
||||
Run directly for local development:
|
||||
|
||||
python -m example_mcp_servers.finance_domain.server
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
from starlette.types import ASGIApp
|
||||
|
||||
from emcp_bus.audit.bypass_detection import BackendCredentialGate
|
||||
from emcp_bus.audit.sink import JSONLFileAuditSink
|
||||
from emcp_bus.common.otel import setup_tracing
|
||||
|
||||
setup_tracing("finance-domain-mcp-server")
|
||||
|
||||
server = MCPServer(name="finance-domain", version="0.1.0")
|
||||
|
||||
_INVOICES: dict[str, dict[str, object]] = {
|
||||
"inv-001": {
|
||||
"invoice_id": "inv-001",
|
||||
"customer_id": "cust-001",
|
||||
"amount": 5000,
|
||||
"status": "open",
|
||||
},
|
||||
"inv-002": {
|
||||
"invoice_id": "inv-002",
|
||||
"customer_id": "cust-002",
|
||||
"amount": 25000,
|
||||
"status": "overdue",
|
||||
},
|
||||
}
|
||||
|
||||
_PAYMENTS: dict[str, dict[str, object]] = {}
|
||||
_payment_ids = itertools.count(1)
|
||||
|
||||
|
||||
@server.tool(name="finance.invoice.get", description="Get a single finance invoice by id.")
|
||||
def invoice_get(invoice_id: str) -> dict[str, object]:
|
||||
invoice = _INVOICES.get(invoice_id)
|
||||
if invoice is None:
|
||||
return {"error": "not_found", "invoice_id": invoice_id}
|
||||
return invoice
|
||||
|
||||
|
||||
@server.tool(
|
||||
name="finance.payment.create", description="Create a draft payment against an invoice."
|
||||
)
|
||||
def payment_create(invoice_id: str, amount: float) -> dict[str, object]:
|
||||
payment_id = f"pay-{next(_payment_ids):03d}"
|
||||
payment = {
|
||||
"payment_id": payment_id,
|
||||
"invoice_id": invoice_id,
|
||||
"amount": amount,
|
||||
"status": "draft",
|
||||
}
|
||||
_PAYMENTS[payment_id] = payment
|
||||
return payment
|
||||
|
||||
|
||||
@server.tool(name="finance.payment.execute", description="Execute a previously created payment.")
|
||||
def payment_execute(payment_id: str, amount: float) -> dict[str, object]:
|
||||
"""`amount` is declared by the caller (not re-derived from the stored
|
||||
payment) so it is visible to `config/policies/transaction.rego`'s
|
||||
`approval_threshold` check (README.md §26) -- the transaction policy
|
||||
inspects call arguments, never backend state, matching `sales.quote.create`'s
|
||||
same pattern (EP-06-T02)."""
|
||||
payment = _PAYMENTS.get(payment_id)
|
||||
if payment is None:
|
||||
return {"error": "not_found", "payment_id": payment_id}
|
||||
payment["status"] = "executed"
|
||||
payment["executed_amount"] = amount
|
||||
return payment
|
||||
|
||||
|
||||
def main() -> None:
|
||||
host = os.environ.get("FINANCE_DOMAIN_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("FINANCE_DOMAIN_PORT", "8101"))
|
||||
expected_token = os.environ.get("FINANCE_DOMAIN_SERVICE_TOKEN")
|
||||
audit_log_path = os.environ.get("EMCP_AUDIT_LOG_PATH", "/tmp/emcp-audit.jsonl")
|
||||
|
||||
app: ASGIApp = server.streamable_http_app(host=host)
|
||||
if expected_token:
|
||||
# EP-05-T07: reject anything that didn't come through the Gateway's
|
||||
# outbound identity (EP-07-T01) with the matching service credential.
|
||||
app = BackendCredentialGate(
|
||||
app,
|
||||
expected_token=expected_token,
|
||||
backend="finance-domain",
|
||||
audit_sink=JSONLFileAuditSink(audit_log_path),
|
||||
)
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Example Sales-domain MCP server (EP-06-T02, walking-skeleton subset).
|
||||
"""Example Sales-domain MCP server (EP-06-T02).
|
||||
|
||||
Read-only tools (risk tier R1, see README.md §17): customer.search,
|
||||
customer.get, order.get, inventory.check. Backed by static mock data -- this
|
||||
server is deliberately "dumb": it never authorizes anything (ADR-023 in
|
||||
docs/RI-PLANNING.md). Authorization happens exclusively upstream, in the
|
||||
Gateway/PEP (EP-05) before a call ever reaches this process.
|
||||
customer.get, order.get, inventory.check. Write tools (R2): quote.create,
|
||||
quote.update. Backed by static mock data -- this server is deliberately
|
||||
"dumb": it never authorizes anything (ADR-023 in docs/RI-PLANNING.md).
|
||||
Authorization happens exclusively upstream, in the Gateway/PEP (EP-05)
|
||||
before a call ever reaches this process.
|
||||
|
||||
Run directly for local development:
|
||||
|
||||
@@ -13,10 +14,14 @@ Run directly for local development:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
from starlette.types import ASGIApp
|
||||
|
||||
from emcp_bus.audit.bypass_detection import BackendCredentialGate
|
||||
from emcp_bus.audit.sink import JSONLFileAuditSink
|
||||
from emcp_bus.common.otel import setup_tracing
|
||||
|
||||
setup_tracing("sales-domain-mcp-server")
|
||||
@@ -37,6 +42,9 @@ _INVENTORY: dict[str, int] = {
|
||||
"sku-200": 0,
|
||||
}
|
||||
|
||||
_QUOTES: dict[str, dict[str, object]] = {}
|
||||
_quote_ids = itertools.count(1)
|
||||
|
||||
|
||||
@server.tool(name="sales.customer.search", description="Search sales customers by name fragment.")
|
||||
def customer_search(query: str) -> dict[str, object]:
|
||||
@@ -65,10 +73,50 @@ def inventory_check(sku: str) -> dict[str, object]:
|
||||
return {"sku": sku, "available": _INVENTORY.get(sku, 0)}
|
||||
|
||||
|
||||
@server.tool(name="sales.quote.create", description="Create a sales quote for a customer.")
|
||||
def quote_create(customer_id: str, amount: float, region: str = "BR-SP") -> dict[str, object]:
|
||||
quote_id = f"quote-{next(_quote_ids):03d}"
|
||||
quote = {
|
||||
"quote_id": quote_id,
|
||||
"customer_id": customer_id,
|
||||
"amount": amount,
|
||||
"region": region,
|
||||
"status": "draft",
|
||||
}
|
||||
_QUOTES[quote_id] = quote
|
||||
return quote
|
||||
|
||||
|
||||
@server.tool(name="sales.quote.update", description="Update an existing sales quote.")
|
||||
def quote_update(quote_id: str, amount: float | None = None) -> dict[str, object]:
|
||||
quote = _QUOTES.get(quote_id)
|
||||
if quote is None:
|
||||
return {"error": "not_found", "quote_id": quote_id}
|
||||
if amount is not None:
|
||||
quote["amount"] = amount
|
||||
return quote
|
||||
|
||||
|
||||
def main() -> None:
|
||||
host = os.environ.get("SALES_DOMAIN_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("SALES_DOMAIN_PORT", "8100"))
|
||||
server.run(transport="streamable-http", host=host, port=port)
|
||||
expected_token = os.environ.get("SALES_DOMAIN_SERVICE_TOKEN")
|
||||
audit_log_path = os.environ.get("EMCP_AUDIT_LOG_PATH", "/tmp/emcp-audit.jsonl")
|
||||
|
||||
app: ASGIApp = server.streamable_http_app(host=host)
|
||||
if expected_token:
|
||||
# EP-05-T07: reject anything that didn't come through the Gateway's
|
||||
# outbound identity (EP-07-T01) with the matching service credential.
|
||||
app = BackendCredentialGate(
|
||||
app,
|
||||
expected_token=expected_token,
|
||||
backend="sales-domain",
|
||||
audit_sink=JSONLFileAuditSink(audit_log_path),
|
||||
)
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user