Task 2 · 8 tasks

Tools over MCP

Give the agent a key to the company database: a query_db tool served by an MCP server over stdio.

25 minMedium
Alice’s ask

The “Hidden Vault” problem

Alice is impressed, but the real department data is locked in a SQLite database. Prompts can't keep up with 1,200 employees. The agent needs a tool, and you'll expose it the standard way: through the Model Context Protocol.

You

Build it

  1. Expose the database as an MCP tool

    Challenge

    Finish the FastMCP server's one tool, query_db(query: str) -> str, so it runs SQL against DB_PATH, and write its description for the model: what it runs, what it returns, and the schema (tables, key columns, and the lowercase status values such as 'active').

    Work in phase1/starter/mcp_server.py; the starter agent in the next step launches it. Reference: phase1/mcp_server.py.

    Hint 1

    FastMCP turns a typed, documented function into a tool with @mcp.tool(). The docstring becomes the description the model reads, so write it for the model: without the table and column names it has to guess them or spend calls looking them up. See FastMCP quickstart.

    Hint 2

    Open the DB with sqlite3.connect, return str(...fetchall()), and return errors as text rather than raising, so the model can read and retry. Run it with mcp.run(transport="stdio").

    Solution

    Paste each block below into phase1/starter/mcp_server.py in place of the definition with the same name (the TODO), including any import line that comes with it. Nothing else in the file changes.

    phase1/starter/mcp_server.py · replace query_db()
    @mcp.tool()
    def query_db(query: str) -> str:
        """Run one SQL statement on the DataStream Corp SQLite database; returns the rows, or the error, as text.
    
        Tables (key columns):
        - departments (id, name, description, manager_id, budget)
        - employees (id, employee_id, first_name, last_name, email, job_title, department_id -> departments.id,
          manager_id -> employees.id, salary, hire_date, status)
        - projects (id, name, description, status, start_date, end_date, budget, department_id, project_manager_id)
        - project_assignments (employee_id -> employees.id, project_id -> projects.id, role, allocation_percentage)
        - user_preferences (user_id, preference_key, preference_value), audit_log (action, table_name, timestamp)
    
        Status values are lowercase: employees.status is 'active' or 'terminated' (headcount counts only 'active'),
        projects.status is 'planning', 'active' or 'completed'. Department names are capitalized, e.g. 'Engineering'.
        """
        try:
            with sqlite3.connect(DB_PATH) as conn:
                return str(conn.execute(query).fetchall())
        except Exception as e:
            return f"Error executing query: {e}"
  2. Connect the agent to it

    Challenge

    Start your server over stdio and give its tools to the agent.

    Work in phase1/starter/t2_mcp_tools.py (look for TODO; an unfinished one prints [starter] TODO …) and run it with uv run bootcamp.py phase1 t2 --starter. The reference solution is phase1/t2_mcp_tools.py; uv run bootcamp.py phase1 t2 runs it.

    Hint 1

    Strands' MCPClient wraps an MCP transport. make_mcp_client() from common.py already builds one; in a --starter run it launches your server. See Strands: MCP tools.

    Hint 2

    The client must be open while the agent runs: use it as a context manager and pass mcp_client.list_tools_sync() as tools.

    Solution

    Paste each block below into phase1/starter/t2_mcp_tools.py in place of the definition with the same name (the TODO), including any import line that comes with it. Nothing else in the file changes.

    phase1/starter/t2_mcp_tools.py · replace build_agent()
    def build_agent(mcp_client: MCPClient) -> Agent:
        """The data engineer: the agent gets every tool the (open) MCP client's server exposes."""
        return Agent(
            name="Data Engineer",
            model=make_model(),
            system_prompt=SYSTEM_PROMPT,
            tools=mcp_client.list_tools_sync(),
        )

    The starter's __main__ already opens make_mcp_client() and passes it in. In a --starter run it launches your phase1/starter/mcp_server.py, and so do the starters of Tasks 3-8 (they stop with a hint while its TODO stub is still there); the reference scripts launch phase1/mcp_server.py.

  3. Run it

    terminal · your starter file
    uv run bootcamp.py phase1 t2 --starter "What tables are in the database?"
    uv run bootcamp.py phase1 t2 --starter "Describe the employees table"
    uv run bootcamp.py phase1 t2 --starter "How many employees are in Engineering?"
    terminal · reference solution
    uv run bootcamp.py phase1 t2 "What tables are in the database?"
    uv run bootcamp.py phase1 t2 "Describe the employees table"
    uv run bootcamp.py phase1 t2 "How many employees are in Engineering?"
  4. Experiments

    • Watch the tool calls. How many queries does the agent need for a count question? Does it look up the schema first?
    • Remove the table list and the status line from your docstring and ask the Engineering question again. Does it need more calls? Does it still filter status = 'active', or does it count the two terminated engineers (483)?
    • This is your copy of the DB, so try a write: “Add a department called Legal”. Nothing stops it. Task 5 fixes that.

Check your work

Phase 1 has no automated test: you check it by running the task and looking for the result below.

“How many employees are in Engineering?” shows a [tool] query_db: SELECT COUNT(*) ... line whose SQL filters status = 'active', and the answer is 481 (483 means it counted the terminated engineers: check the status line of your docstring). “What tables are in the database?” names departments, employees, projects, project_assignments, user_preferences and audit_log, either from a sqlite_master query or straight from the tool description (then without a [tool] line). “Describe the employees table” lists columns such as employee_id, first_name, last_name, job_title, department_id and status.

Failed? Run the same prompt on the reference (drop --starter). If the reference fails too, it's the model: rerun or try another MODEL_ID (uv run bootcamp.py llm models).

Under the hood

MCP separates who has the tools from who uses them. Here the transport is stdio: make_mcp_client() spawns mcp_server.py (yours in a --starter run) with the same Python and talks JSON-RPC over its stdin/stdout. Strands converts each MCP tool schema into a tool spec the model can call. In Phase 2 the same server moves to AgentCore Runtime and the transport becomes streamable HTTP.