Building Your Own MCP Server for AI

Resources vs Tools

Expose read-only data as an MCP resource and keep mutations on tools — when each shape is right.

A tool is “do this.” A resource is “here is a document at this URI.” If the assistant only needs to read a snapshot, a resource is often cleaner: the host can fetch it without inventing arguments.

Use a resource when

  • The data is read-only for this session (a config file, a schema, today’s menu).
  • You can name it with a stable URI (config://app, notes://inbox).
  • The model should treat it like a file, not like a function with five optional filters.

Use a tool when

  • Something changes (create, update, send, restart).
  • You need arguments that are not a single URI (date ranges, search strings).
  • The call is expensive or rate-limited and should be explicit.

A resource next to a tool

from mcp.server.mcpserver import MCPServer

mcp = MCPServer("notes")

NOTES = {"welcome": "First note. Replace this in the next lesson."}


@mcp.resource("notes://welcome")
def welcome_note() -> str:
    '''The welcome note as plain text.'''
    return NOTES["welcome"]


@mcp.tool()
def set_welcome(text: str) -> str:
    '''Replace the welcome note. Use this when the user wants it changed.'''
    NOTES["welcome"] = text.strip()
    return "updated"

The resource URI is not an HTTP URL. It is an MCP identifier the host understands. After set_welcome, a later read of notes://welcome should show the new text (in-memory only — restart clears it).

Next: reusable MCP prompts.