Module 11 · cognitive feature

Dynamic tool synthesis over MCP

Hardcoding every enterprise API an agent might need is impossible. Hand it the OpenAPI spec instead: operations become MCP descriptors, scopes decide what is exposed, and a denylist keeps dangerous operations out of the manifest entirely. A tool the model never sees is a tool it cannot be talked into calling.

$ python -m fde_toolkit tools

Granted scopes

the agent's service principal

Denylist

never exposed, whatever the scope

Synthesised tool manifest

3 of 4 operations exposed

  • exposedget_customerGET /customers/{customerId}

    Fetch a customer dossier including KYC status.

    scopes: customer.read · risk low

    {
      "name": "get_customer",
      "description": "Fetch a customer dossier including KYC status.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string",
            "description": "path parameter"
          }
        },
        "required": [
          "customerId"
        ]
      },
      "annotations": {
        "readOnlyHint": true,
        "destructiveHint": false,
        "requiresApproval": false
      }
    }
  • exposedlist_transactionsGET /accounts/{accountId}/transactions

    List transactions for an account within a date range.

    scopes: transactions.read · risk low

    {
      "name": "list_transactions",
      "description": "List transactions for an account within a date range.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "accountId": {
            "type": "string",
            "description": "path parameter"
          },
          "from": {
            "type": "string",
            "description": "query parameter"
          },
          "to": {
            "type": "string",
            "description": "query parameter"
          },
          "minAmount": {
            "type": "number",
            "description": "query parameter"
          }
        },
        "required": [
          "accountId"
        ]
      },
      "annotations": {
        "readOnlyHint": true,
        "destructiveHint": false,
        "requiresApproval": false
      }
    }
  • exposedcreate_paymentPOST /paymentsapproval

    Initiate an outbound payment.

    scopes: payments.write · risk high

    {
      "name": "create_payment",
      "description": "Initiate an outbound payment.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "sourceAccount": {
            "type": "string"
          },
          "beneficiary": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          }
        },
        "required": [
          "amount",
          "beneficiary",
          "currency",
          "sourceAccount"
        ]
      },
      "annotations": {
        "readOnlyHint": false,
        "destructiveHint": true,
        "requiresApproval": true
      }
    }
  • withhelddebug_dumpGET /internal/debug/dump

    Dump raw table contents for support engineers.

    scopes: admin · risk high

    On the FDE denylist: never exposed autonomously.

Call synthesis

natural language → HTTP

GETlist_transactions

https://corebank.internal/api/v11/accounts/AE070331234567890123456/transactions?from=2026-01-01&to=2026-03-31

  • · Read-only; the date range was inferred from 'this quarter'.

Why this beats glue code

  • No redeploy per endpoint. The client publishes a new operation and the agent discovers it.
  • Authorisation stays where it belongs. Revoke a scope and the tool disappears from the manifest — no prompt edit.
  • Annotations carry intent. readOnlyHint and requiresApproval let the orchestrator gate writes without parsing the description.
  • Refusal is structural. Turn the denylist off above and watch a dangerous operation enter the manifest — that is the demo to run for a security team.