{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.13.0"
    },
    "blog_metadata": {
      "topic": "How Fabric Local MCP Could Turn Microsoft Fabric Into an Agent Runtime",
      "slug": "how-fabric-local-mcp-could-turn-microsoft-fabric-into-an-age",
      "generated_by": "LinkedIn Post Generator + Azure OpenAI",
      "generated_at": "2026-08-10T19:14:49.460Z"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# How Fabric Local MCP Could Turn Microsoft Fabric Into an Agent Runtime\n",
        "\n",
        "This notebook turns the blog post into a hands-on validation flow focused on safe, narrow, auditable agent-tool interactions. The emphasis is not on broad autonomy, but on proving how policy gates, scoped tool contracts, local MCP endpoints, and audit metadata can work together as a governed execution surface."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "%pip install requests"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "from dataclasses import dataclass, asdict\n",
        "from typing import List, Dict, Optional\n",
        "from datetime import datetime, timezone\n",
        "from http.server import BaseHTTPRequestHandler, HTTPServer\n",
        "import threading\n",
        "import requests\n",
        "import json\n",
        "import uuid\n",
        "import os\n",
        "import shutil\n",
        "import subprocess\n",
        "import sys\n",
        "import time"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Architecture sketch\n",
        "\n",
        "This cell captures the core control-plane idea from the post: policy should sit between the agent and Fabric-facing tools. Treat the diagram as a design reference for the validation steps that follow."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "architecture_flow = r'''\n",
        "flowchart TD\n",
        "    U[User Prompt] --> A[Foundry Agent]\n",
        "    A --> P[Policy Guardrails]\n",
        "    P -->|Allowed task only| M[Fabric Local MCP Endpoint]\n",
        "    M --> T[Fabric Tool Adapter]\n",
        "    T --> F[Microsoft Fabric APIs]\n",
        "    F --> T\n",
        "    T --> M\n",
        "    M --> A\n",
        "    A --> L[Audit Log / Telemetry]\n",
        "    A --> R[Final Response]\n",
        "'''\n",
        "\n",
        "print(architecture_flow)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Conceptual agent configuration\n",
        "\n",
        "This example defines a narrow tool contract for a Fabric-oriented agent. The key validation point is that the agent is explicitly limited to a small set of approved tasks and required audit fields."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "from dataclasses import dataclass, asdict\n",
        "from typing import List\n",
        "import json\n",
        "\n",
        "@dataclass\n",
        "class MCPToolConfig:\n",
        "    name: str\n",
        "    endpoint: str\n",
        "    allowed_tasks: List[str]\n",
        "    audit_fields: List[str]\n",
        "\n",
        "agent_config = {\n",
        "    \"agent_name\": \"fabric-ops-agent\",\n",
        "    \"model\": \"gpt-4.1\",\n",
        "    \"instructions\": \"Use Fabric tools only for dataset refresh status and capacity inspection.\",\n",
        "    \"tools\": [\n",
        "        asdict(MCPToolConfig(\n",
        "            name=\"fabric-local-mcp\",\n",
        "            endpoint=\"http://127.0.0.1:3001/mcp\",\n",
        "            allowed_tasks=[\"get_refresh_status\", \"list_capacities\"],\n",
        "            audit_fields=[\"user_id\", \"tool_name\", \"task\", \"timestamp\", \"correlation_id\"]\n",
        "        ))\n",
        "    ]\n",
        "}\n",
        "\n",
        "print(json.dumps(agent_config, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Minimal policy gate\n",
        "\n",
        "This example classifies prompts into a small approved task set and fails closed when the request is out of scope. That is the core governance move: do not let the model decide access boundaries at runtime."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "ALLOWED_TASKS = {\n",
        "    \"get_refresh_status\": [\"refresh\", \"dataset\", \"status\"],\n",
        "    \"list_capacities\": [\"capacity\", \"capacities\", \"sku\"]\n",
        "}\n",
        "\n",
        "def classify_task(prompt: str) -> Optional[str]:\n",
        "    text = prompt.lower()\n",
        "    for task, keywords in ALLOWED_TASKS.items():\n",
        "        if any(word in text for word in keywords):\n",
        "            return task\n",
        "    return None\n",
        "\n",
        "prompts = [\n",
        "    \"Check the latest dataset refresh status for SalesSemanticModel\",\n",
        "    \"List current capacities and SKU assignments\",\n",
        "    \"Edit the semantic model and add a new measure\"\n",
        "]\n",
        "\n",
        "results = []\n",
        "for prompt in prompts:\n",
        "    task = classify_task(prompt)\n",
        "    results.append({\n",
        "        \"prompt\": prompt,\n",
        "        \"approved_task\": task,\n",
        "        \"allowed\": task is not None\n",
        "    })\n",
        "\n",
        "print(json.dumps(results, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for local MCP authentication\n",
        "\n",
        "If you later connect this notebook to a real local MCP process or Fabric-backed service, these are the typical variables to define:\n",
        "\n",
        "- FABRIC_MCP_AUTH_MODE\n",
        "- FABRIC_TENANT_ID\n",
        "- FABRIC_CLIENT_ID\n",
        "- FABRIC_CLIENT_SECRET\n",
        "- FABRIC_WORKSPACE_ID\n",
        "\n",
        "For this notebook, the values are only inspected and not used to call Microsoft services."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "required_env_vars = [\n",
        "    \"FABRIC_MCP_AUTH_MODE\",\n",
        "    \"FABRIC_TENANT_ID\",\n",
        "    \"FABRIC_CLIENT_ID\",\n",
        "    \"FABRIC_CLIENT_SECRET\",\n",
        "    \"FABRIC_WORKSPACE_ID\"\n",
        "]\n",
        "\n",
        "status = {name: (\"set\" if os.getenv(name) else \"missing\") for name in required_env_vars}\n",
        "print(json.dumps(status, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Audit wrapper for tool calls\n",
        "\n",
        "This example records attributable metadata around a conceptual MCP invocation. The validation goal is to prove that every tool action can be tied to a user, task, timestamp, and correlation ID."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "from datetime import datetime, timezone\n",
        "import json\n",
        "import uuid\n",
        "\n",
        "def call_mcp_tool(task: str, payload: dict, user_id: str) -> dict:\n",
        "    correlation_id = str(uuid.uuid4())\n",
        "    audit = {\n",
        "        \"user_id\": user_id,\n",
        "        \"tool_name\": \"fabric-local-mcp\",\n",
        "        \"task\": task,\n",
        "        \"timestamp\": datetime.now(timezone.utc).isoformat(),\n",
        "        \"correlation_id\": correlation_id,\n",
        "    }\n",
        "    result = {\"ok\": True, \"task\": task, \"data\": {\"status\": \"Completed\", \"payload\": payload}}\n",
        "    print(json.dumps({\"audit\": audit, \"result_summary\": result[\"ok\"]}, indent=2))\n",
        "    return result\n",
        "\n",
        "response = call_mcp_tool(\"get_refresh_status\", {\"dataset\": \"SalesSemanticModel\"}, \"alice@contoso.com\")\n",
        "print(json.dumps(response, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Local prerequisite check\n",
        "\n",
        "The original post used PowerShell to verify Node.js for local MCP hosting. This notebook uses Python to perform the same validation so it can run directly in a Python notebook environment."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def check_node_prerequisite(minimum_major: int = 18) -> dict:\n",
        "    node_path = shutil.which(\"node\")\n",
        "    if not node_path:\n",
        "        return {\n",
        "            \"node_found\": False,\n",
        "            \"message\": f\"Node.js is required for local MCP hosting. Install Node.js {minimum_major}+.\",\n",
        "        }\n",
        "\n",
        "    completed = subprocess.run([node_path, \"--version\"], capture_output=True, text=True)\n",
        "    version_text = completed.stdout.strip().lstrip(\"v\")\n",
        "    major = int(version_text.split(\".\")[0])\n",
        "    return {\n",
        "        \"node_found\": True,\n",
        "        \"node_path\": node_path,\n",
        "        \"version\": version_text,\n",
        "        \"meets_minimum\": major >= minimum_major,\n",
        "        \"minimum_major\": minimum_major,\n",
        "    }\n",
        "\n",
        "print(json.dumps(check_node_prerequisite(), indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for a bounded pilot\n",
        "\n",
        "For a tightly scoped pilot, document the authentication and workspace boundary explicitly. In a real deployment, these values should come from a secure secret store or managed identity flow rather than being hard-coded."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "example_env = {\n",
        "    \"FABRIC_MCP_AUTH_MODE\": os.getenv(\"FABRIC_MCP_AUTH_MODE\", \"service-principal\"),\n",
        "    \"FABRIC_TENANT_ID\": os.getenv(\"FABRIC_TENANT_ID\", \"<tenant-id>\"),\n",
        "    \"FABRIC_CLIENT_ID\": os.getenv(\"FABRIC_CLIENT_ID\", \"<client-id>\"),\n",
        "    \"FABRIC_CLIENT_SECRET\": \"<redacted-if-set>\",\n",
        "    \"FABRIC_WORKSPACE_ID\": os.getenv(\"FABRIC_WORKSPACE_ID\", \"00000000-0000-0000-0000-000000000000\")\n",
        "}\n",
        "\n",
        "safe_view = dict(example_env)\n",
        "if os.getenv(\"FABRIC_CLIENT_SECRET\"):\n",
        "    safe_view[\"FABRIC_CLIENT_SECRET\"] = \"set\"\n",
        "\n",
        "print(json.dumps(safe_view, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Tiny local MCP endpoint stub\n",
        "\n",
        "This example starts a minimal local HTTP server that only accepts two approved tasks. It is intentionally narrow so you can validate the runtime shape without exposing a broad action surface."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import json\n",
        "from http.server import BaseHTTPRequestHandler, HTTPServer\n",
        "import threading\n",
        "\n",
        "class MCPHandler(BaseHTTPRequestHandler):\n",
        "    def do_POST(self):\n",
        "        if self.path != \"/mcp\":\n",
        "            self.send_response(404)\n",
        "            self.end_headers()\n",
        "            return\n",
        "\n",
        "        content_length = int(self.headers.get(\"Content-Length\", \"0\"))\n",
        "        body = json.loads(self.rfile.read(content_length) or b\"{}\")\n",
        "        task = body.get(\"task\")\n",
        "\n",
        "        if task not in {\"get_refresh_status\", \"list_capacities\"}:\n",
        "            self.send_response(403)\n",
        "            self.send_header(\"Content-Type\", \"application/json\")\n",
        "            self.end_headers()\n",
        "            self.wfile.write(json.dumps({\"ok\": False, \"error\": \"task_not_allowed\", \"task\": task}).encode())\n",
        "            return\n",
        "\n",
        "        response = {\"ok\": True, \"task\": task, \"data\": {\"message\": \"Conceptual Fabric result\"}}\n",
        "        self.send_response(200)\n",
        "        self.send_header(\"Content-Type\", \"application/json\")\n",
        "        self.end_headers()\n",
        "        self.wfile.write(json.dumps(response).encode())\n",
        "\n",
        "    def log_message(self, format, *args):\n",
        "        return\n",
        "\n",
        "server = HTTPServer((\"127.0.0.1\", 3001), MCPHandler)\n",
        "thread = threading.Thread(target=server.serve_forever, daemon=True)\n",
        "thread.start()\n",
        "\n",
        "print(\"MCP endpoint started at http://127.0.0.1:3001/mcp\")"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Validate the local MCP endpoint\n",
        "\n",
        "This cell sends both an allowed task and a blocked task to the local endpoint. The expected behavior is a 200 response for approved operations and a 403 response for anything outside the contract."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "allowed = requests.post(\"http://127.0.0.1:3001/mcp\", json={\"task\": \"get_refresh_status\"}, timeout=5)\n",
        "blocked = requests.post(\"http://127.0.0.1:3001/mcp\", json={\"task\": \"edit_semantic_model\"}, timeout=5)\n",
        "\n",
        "validation = {\n",
        "    \"allowed_status_code\": allowed.status_code,\n",
        "    \"allowed_body\": allowed.json(),\n",
        "    \"blocked_status_code\": blocked.status_code,\n",
        "    \"blocked_body\": blocked.json()\n",
        "}\n",
        "\n",
        "print(json.dumps(validation, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## End-to-end governed call simulation\n",
        "\n",
        "This cell combines prompt classification, policy enforcement, local MCP invocation, and audit logging into one flow. It demonstrates the safe pattern described in the post: classify first, call tools second, and record metadata as part of the transaction."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def governed_agent_call(prompt: str, user_id: str) -> dict:\n",
        "    task = classify_task(prompt)\n",
        "    if task is None:\n",
        "        return {\n",
        "            \"ok\": False,\n",
        "            \"reason\": \"Prompt is outside the allowed MCP task scope.\",\n",
        "            \"prompt\": prompt,\n",
        "            \"user_id\": user_id,\n",
        "        }\n",
        "\n",
        "    correlation_id = str(uuid.uuid4())\n",
        "    audit = {\n",
        "        \"user_id\": user_id,\n",
        "        \"tool_name\": \"fabric-local-mcp\",\n",
        "        \"task\": task,\n",
        "        \"timestamp\": datetime.now(timezone.utc).isoformat(),\n",
        "        \"correlation_id\": correlation_id,\n",
        "    }\n",
        "\n",
        "    r = requests.post(\n",
        "        \"http://127.0.0.1:3001/mcp\",\n",
        "        json={\"task\": task, \"payload\": {\"prompt\": prompt}, \"correlation_id\": correlation_id},\n",
        "        timeout=5,\n",
        "    )\n",
        "\n",
        "    return {\n",
        "        \"ok\": r.status_code == 200,\n",
        "        \"audit\": audit,\n",
        "        \"tool_response\": r.json(),\n",
        "    }\n",
        "\n",
        "approved_run = governed_agent_call(\"Check dataset refresh status for SalesSemanticModel\", \"alice@contoso.com\")\n",
        "rejected_run = governed_agent_call(\"Edit the semantic model in production\", \"alice@contoso.com\")\n",
        "\n",
        "print(json.dumps({\"approved_run\": approved_run, \"rejected_run\": rejected_run}, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Sequence sketch\n",
        "\n",
        "This sequence captures the intended transaction path: user request, policy validation, MCP tool call, Fabric interaction, audit recording, and final response. It is useful for architecture reviews and control discussions."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "sequence_diagram = r'''\n",
        "sequenceDiagram\n",
        "    participant User\n",
        "    participant Agent as Foundry Agent\n",
        "    participant Policy as Scope Policy\n",
        "    participant MCP as Fabric Local MCP\n",
        "    participant Fabric as Fabric API\n",
        "    participant Audit as Audit Store\n",
        "\n",
        "    User->>Agent: \"Check dataset refresh status\"\n",
        "    Agent->>Policy: Classify + validate task\n",
        "    Policy-->>Agent: Allowed: get_refresh_status\n",
        "    Agent->>MCP: Tool call with correlation ID\n",
        "    MCP->>Fabric: Query refresh state\n",
        "    Fabric-->>MCP: Completed\n",
        "    MCP-->>Agent: Tool result\n",
        "    Agent->>Audit: Record metadata + outcome\n",
        "    Agent-->>User: Final answer\n",
        "'''\n",
        "\n",
        "print(sequence_diagram)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Governance scorecard\n",
        "\n",
        "The blog ends with a practical question: if an agent edited a semantic model in production tomorrow, could your team prove who authorized it, what tool path it used, and what it cost? Use this simple scorecard to rate current readiness from 1 to 5."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "scorecard = {\n",
        "    \"identity_attribution\": 0,\n",
        "    \"workspace_scope_control\": 0,\n",
        "    \"task_level_policy_gates\": 0,\n",
        "    \"audit_completeness\": 0,\n",
        "    \"cost_visibility\": 0\n",
        "}\n",
        "\n",
        "print(\"Rate each category from 1 to 5.\")\n",
        "print(json.dumps(scorecard, indent=2))\n",
        "print(\"Suggested interpretation: 1-2 = weak controls, 3 = partial, 4-5 = strong pilot readiness\")"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Next Steps\n",
        "\n",
        "This notebook validated the core pattern behind Fabric Local MCP as an agent runtime surface: narrow tool scope, explicit policy checks, local endpoint control, and attributable audit metadata. The main lesson is that governance is the product requirement, not an afterthought.\n",
        "\n",
        "Next steps:\n",
        "- Replace the conceptual local endpoint with a real MCP-compatible service in a sandbox.\n",
        "- Bind authentication to a tightly scoped identity and single workspace.\n",
        "- Expand audit logging to a durable store with correlation IDs and cost fields.\n",
        "- Add approval workflows before any write-capable semantic model actions.\n",
        "- Pilot one read-focused operational workflow before considering broader autonomy."
      ]
    }
  ]
}