{"slug": "mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris", "title": "MCP Series (05): Resources and Prompts Deep Dive — Dynamic Data, Parameterized URIs, and Multi-Turn Templates", "summary": "A developer detailed the distinction between Tools and Resources in the Model Context Protocol (MCP), explaining that Resources are read-only data sources injected by the host while Tools are actions executed by the LLM. The post demonstrated dynamic Resources that return live data on each read, parameterized URIs for handling multiple instances, and URI design principles for hierarchical organization.", "body_md": "The split:\n\n```\nTools      → actions the LLM executes (verbs)\n             LLM decides when to call; calls may have side effects\n             Examples: create_issue, update_status\n\nResources  → data the LLM reads (nouns)\n             Host decides when to inject; read-only, no side effects\n             Examples: current Sprint status, project statistics\n```\n\nThe rule: \"reading a state\" → Resource. \"Executing an operation\" → Tool. The same data can have both: `get_issue`\n\nas a Tool (LLM controls when to call it), `jira://issue/PROJ-101`\n\nas a Resource (Host injects automatically when relevant).\n\nA static Resource returns the same data every time (like a project list). A dynamic Resource returns the current state on each read — content changes as the underlying data changes.\n\n**Sprint status: every read returns live data**\n\n``` python\n_sprint_progress_pct = 65\n\n@server.read_resource()\nasync def read_resource(uri: str) -> str:\n    if str(uri) == \"jira://sprint/current\":\n        global _sprint_progress_pct\n        _sprint_progress_pct = min(100, _sprint_progress_pct + random.randint(0, 3))\n\n        return json.dumps({\n            \"sprint_name\": \"Sprint 42\",\n            \"progress_pct\": _sprint_progress_pct,                    # ← different each time\n            \"last_updated\": datetime.now(timezone.utc).isoformat(),  # ← timestamp changes\n            \"days_remaining\": 5,\n            \"p0_open\": count_p0_open(),                              # ← tracks live state\n        }, indent=2)\n```\n\n**Test output:**\n\n```\nRead 1: progress=65%  last_updated=...62+00:00\nRead 2: progress=67%  last_updated=...04+00:00\n→ ✓ data changed between reads\n```\n\nHardcoding sprint progress in a Prompt means the LLM works from a stale snapshot. A Dynamic Resource gives it the current number on every read.\n\nMark the Resource as dynamic in its description so the LLM knows to re-read when it needs fresh data:\n\n```\nResource(\n    uri=\"jira://sprint/current\",\n    description=(\n        \"Live status of the active sprint: progress, issue counts. \"\n        \"Read when the user asks about sprint health. \"\n        \"Re-read if you need up-to-date data — content changes over time.\"\n        # ↑ explicit signal that this is dynamic\n    ),\n)\n```\n\nWhen one Resource type has many instances, use parameterized URIs. `list_resources()`\n\nenumerates all instances; `read_resource()`\n\nuses a single handler for all of them.\n\n**One stats Resource per project:**\n\n``` php\n@server.list_resources()\nasync def list_resources() -> list[Resource]:\n    resources = []\n    for key, proj in PROJECTS.items():\n        resources.append(Resource(\n            uri=f\"jira://project/{key}/stats\",\n            name=f\"{proj['name']} Stats\",\n            description=f\"Issue statistics for {proj['name']} ({key}).\",\n        ))\n    return resources\n\n@server.read_resource()\nasync def read_resource(uri: str) -> str:\n    if str(uri).startswith(\"jira://project/\") and str(uri).endswith(\"/stats\"):\n        proj_key = str(uri).split(\"/\")[3].upper()  # parse from jira://project/{key}/stats\n\n        if proj_key not in PROJECTS:\n            raise ValueError(f\"Unknown project: {proj_key}\")\n\n        proj_issues = [i for i in ISSUES.values() if i[\"project\"] == proj_key]\n        return json.dumps({\n            \"project\": proj_key,\n            \"total\": len(proj_issues),\n            \"by_status\": count_by(proj_issues, \"status\"),\n            \"by_priority\": count_by(proj_issues, \"priority\"),\n        }, indent=2)\n```\n\n**Test output:**\n\n```\njira://project/PROJ/stats   → total=3, by_status={'Open': 2, 'In Progress': 1}\njira://project/MOBILE/stats → total=1, by_status={'Open': 1}\njira://project/INFRA/stats  → total=1, by_status={'Done': 1}\n```\n\nThe LLM reads only the project it needs. The Host can also inject the right Resource based on current context — if the user is working in the MOBILE project, inject `MOBILE/stats`\n\nrather than dumping all projects at once.\n\n**URI design principles:**\n\n```\njira://project/{key}/stats    ← hierarchical path (like REST)\njira://sprint/current         ← active instance, no ID needed\njira://dashboard              ← aggregate view, fixed URI\n\nAvoid:\njira://stats_PROJ             ← flat, doesn't scale\njira://data?project=PROJ      ← query params, harder to parse\n```\n\nA Prompt template doesn't have to be static text. Render different sections based on argument values so one Prompt covers multiple scenarios cleanly.\n\n**Incident report: P0 includes Escalation section, P1 doesn't**\n\n```\nif name == \"incident_report\":\n    severity = args.get(\"severity\", \"P1\").upper()\n    workaround = args.get(\"workaround\", \"\")\n\n    # conditional section: P0 only\n    p0_section = \"\"\n    if severity == \"P0\":\n        p0_section = (\n            \"\\n## Escalation\\n\"\n            \"- Engineering VP: notify within 30 minutes\\n\"\n            \"- SLA breach risk: may breach the 4-hour P0 SLA\\n\"\n        )\n\n    # conditional section: only when workaround provided\n    workaround_section = \"\"\n    if workaround:\n        workaround_section = f\"\\n## Workaround\\n{workaround}\\n\"\n\n    template = (\n        f\"Create a formal incident report for {issue_key}...\\n\"\n        f\"## Summary\\n...\\n\"\n        f\"## Root Cause\\n...\"\n        f\"{p0_section}\"          # ← conditional insert\n        f\"{workaround_section}\"  # ← conditional insert\n    )\n```\n\n**Test output:**\n\n```\nP0: escalation_section=✓  workaround_section=✗\nP1: escalation_section=✗  workaround_section=✓\n```\n\nP0 incidents trigger escalation protocol; P1 incidents show the workaround. The LLM receives a different template and generates a structurally different report. No need for a single large template that the LLM has to interpret.\n\nStandard Prompts have one user message. Multi-turn Prompts pre-fill a conversation history to guide the LLM through specific steps before producing the final output.\n\n**PR description: 3 turns, second turn is an assistant 'thinking' step**\n\n```\nif name == \"pr_description\":\n    return GetPromptResult(\n        messages=[\n            # Turn 1: user sets context\n            PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=(\n                f\"You are a senior engineer writing a PR description.\\n\"\n                f\"PR addresses: {issue_key}\\n\\n\"\n                f\"First, use get_issue to read the Jira issue details.\"\n            ))),\n            # Turn 2: pre-filled assistant thinking step\n            PromptMessage(role=\"assistant\", content=TextContent(type=\"text\", text=(\n                \"I'll fetch the issue details and then write a PR description \"\n                \"with: title, motivation, changes summary, test plan, and links.\"\n            ))),\n            # Turn 3: final instruction\n            PromptMessage(role=\"user\", content=TextContent(type=\"text\", text=(\n                \"Now write the complete PR description in Markdown.\"\n            ))),\n        ]\n    )\n```\n\n**Test output:**\n\n```\nTurn count: 3\nTurn 1 (user):      You are a senior engineer writing a PR description...\nTurn 2 (assistant): I'll fetch the issue details and then write a PR description...\nTurn 3 (user):      Now write the complete PR description in Markdown.\n```\n\n**Uses for multi-turn Prompts:**\n\nWhen a Prompt template embeds instructions to \"read this Resource\" or \"call this Tool,\" the user doesn't need to supply data — the template tells the LLM how to get it.\n\n```\nif name == \"standup_update\":\n    return GetPromptResult(messages=[PromptMessage(role=\"user\",\n        content=TextContent(type=\"text\", text=(\n            f\"Generate a daily standup update for {team_member}.\\n\\n\"\n            f\"Steps:\\n\"\n            f\"1. Read jira://sprint/current to see overall sprint health\\n\"  # ← Resource ref\n            f\"2. Use search_issues to find issues {team_member} worked on\\n\"  # ← Tool ref\n            f\"3. Write standup: Yesterday / Today / Blockers / Sprint health\"\n        ))\n    )])\n```\n\n**Test output:**\n\n```\nReferences resource: ✓  (jira://sprint/current in template)\nReferences tool:     ✓  (search_issues in template)\n```\n\nThe LLM receives this Prompt and automatically reads `jira://sprint/current`\n\n, calls `search_issues`\n\n, then generates the standup. The user only needs to say \"generate my standup\" — no manual data gathering required.\n\nResources and Tools aren't mutually exclusive. A Prompt can reference both:\n\n```\nLLM executing standup_update:\n\n1. Read Resource  jira://sprint/current → overall sprint health\n2. Call Tool      search_issues(query=\"closed\", assignee=\"alice\") → completed yesterday\n3. Call Tool      search_issues(query=\"open\", assignee=\"alice\") → planned today\n4. Generate       combine data into standup format\n```\n\nResources inject background context (passive, no side effects). Tools execute queries or operations (active, LLM-initiated, may have side effects). Each type handles what it's designed for.\n\n**Resources**\n\n**Prompts**\n\n*Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.*\n\n*Find more useful knowledge and interesting products on my Homepage*", "url": "https://wpnews.pro/news/mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris", "canonical_source": "https://dev.to/wonderlab/mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris-and-multi-turn-243k", "published_at": "2026-07-13 03:35:48+00:00", "updated_at": "2026-07-13 03:43:19.000166+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "ai-agents"], "entities": ["MCP", "JIRA"], "alternates": {"html": "https://wpnews.pro/news/mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris", "markdown": "https://wpnews.pro/news/mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris.md", "text": "https://wpnews.pro/news/mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris.txt", "jsonld": "https://wpnews.pro/news/mcp-series-05-resources-and-prompts-deep-dive-dynamic-data-parameterized-uris.jsonld"}}