MCP Course: Error when running tiny-agents example

Error when running tiny-agents example

I’m following the MCP course and I’m stuck at the end of the MCP Clients section. When running the command “tiny-agents run agent.json” I get the error below.
I followed all instructions. What am I missing?

An unexpected error occurred: ‘config’
Traceback (most recent call last):
File “C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py”, line 134, in run_agent
await agent.load_tools()
File “C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\agent.py”, line 57, in load_tools
await self.add_mcp_server(cfg[“type”], **cfg[“config”])
~~~^^^^^^^^^^
KeyError: ‘config’

An unexpected error occurred: ‘config’
╭───────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────╮
│ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:225 in run │
│ │
│ 222 │ │ raise typer.Exit(code=130) │
│ 223 │ except Exception as e: │
│ 224 │ │ print(f"\n[bold red]An unexpected error occurred: {e}[/bold red]“, flush=True) │
│ ❱ 225 │ │ raise e │
│ 226 │
│ 227 │
│ 228 if name == “main”: │
│ │
│ ╭────── locals ───────╮ │
│ │ path = ‘agent.json’ │ │
│ ╰─────────────────────╯ │
│ │
│ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:219 in run │
│ │
│ 216 │ ), │
│ 217 ): │
│ 218 │ try: │
│ ❱ 219 │ │ asyncio.run(run_agent(path)) │
│ 220 │ except KeyboardInterrupt: │
│ 221 │ │ print(”\n[red]Application terminated by KeyboardInterrupt.[/red]“, flush=True) │
│ 222 │ │ raise typer.Exit(code=130) │
│ │
│ ╭────── locals ───────╮ │
│ │ path = ‘agent.json’ │ │
│ ╰─────────────────────╯ │
│ │
│ C:\Python312\Lib\asyncio\runners.py:194 in run │
│ │
│ 191 │ │ │ “asyncio.run() cannot be called from a running event loop”) │
│ 192 │ │
│ 193 │ with Runner(debug=debug, loop_factory=loop_factory) as runner: │
│ ❱ 194 │ │ return runner.run(main) │
│ 195 │
│ 196 │
│ 197 def _cancel_all_tasks(loop): │
│ │
│ ╭─────────────────────────────── locals ───────────────────────────────╮ │
│ │ debug = None │ │
│ │ loop_factory = None │ │
│ │ main = <coroutine object run_agent at 0x000001F738D7C9F0> │ │
│ │ runner = <asyncio.runners.Runner object at 0x000001F739D026C0> │ │
│ ╰──────────────────────────────────────────────────────────────────────╯ │
│ │
│ C:\Python312\Lib\asyncio\runners.py:118 in run │
│ │
│ 115 │ │ │
│ 116 │ │ self._interrupt_count = 0 │
│ 117 │ │ try: │
│ ❱ 118 │ │ │ return self._loop.run_until_complete(task) │
│ 119 │ │ except exceptions.CancelledError: │
│ 120 │ │ │ if self._interrupt_count > 0: │
│ 121 │ │ │ │ uncancel = getattr(task, “uncancel”, None) │
│ │
│ ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │
│ │ context = <_contextvars.Context object at 0x000001F739D12840> │ │
│ │ coro = <coroutine object run_agent at 0x000001F738D7C9F0> │ │
│ │ self = <asyncio.runners.Runner object at 0x000001F739D026C0> │ │
│ │ sigint_handler = functools.partial(<bound method Runner._on_sigint of <asyncio.runners.Runner object at │ │
│ │ 0x000001F739D026C0>>, main_task=<Task finished name=‘Task-1’ coro=<run_agent() done, defined at │ │
│ │ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:28> │ │
│ │ exception=KeyError(‘config’)>) │ │
│ │ task = <Task finished name=‘Task-1’ coro=<run_agent() done, defined at │ │
│ │ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:28> │ │
│ │ exception=KeyError(‘config’)> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ C:\Python312\Lib\asyncio\base_events.py:687 in run_until_complete │
│ │
│ 684 │ │ if not future.done(): │
│ 685 │ │ │ raise RuntimeError(‘Event loop stopped before Future completed.’) │
│ 686 │ │ │
│ ❱ 687 │ │ return future.result() │
│ 688 │ │
│ 689 │ def stop(self): │
│ 690 │ │ “”“Stop running the event loop. │
│ │
│ ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │
│ │ future = <Task finished name=‘Task-1’ coro=<run_agent() done, defined at │ │
│ │ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:28> │ │
│ │ exception=KeyError(‘config’)> │ │
│ │ new_task = False │ │
│ │ self = │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:194 in run_agent │
│ │
│ 191 │ except Exception as e: │
│ 192 │ │ tb_str = traceback.format_exc() │
│ 193 │ │ print(f”\n[bold red]An unexpected error occurred: {e}\n{tb_str}[/bold red]”, flu │
│ ❱ 194 │ │ raise e │
│ 195 │ │
│ 196 │ finally: │
│ 197 │ │ if sigint_registered_in_loop: │
│ │
│ ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │
│ │ abort_event = <asyncio.locks.Event object at 0x000001F739DE9340 [unset]> │ │
│ │ agent = <huggingface_hub.inference._mcp.agent.Agent object at 0x000001F739DE93A0> │ │
│ │ agent_path = ‘agent.json’ │ │
│ │ config = { │ │
│ │ │ ‘model’: ‘Qwen/Qwen2.5-72B-Instruct’, │ │
│ │ │ ‘provider’: ‘nebius’, │ │
│ │ │ ‘servers’: [ │ │
│ │ │ │ {‘type’: ‘stdio’, ‘command’: ‘npx’, ‘args’: [‘@playwright/mcp@latest’]} │ │
│ │ │ ] │ │
│ │ } │ │
│ │ exit_event = <asyncio.locks.Event object at 0x000001F739DE9370 [unset]> │ │
│ │ first_sigint = True │ │
│ │ inputs = │ │
│ │ loop = │ │
│ │ original_sigint_handler = functools.partial(<bound method Runner._on_sigint of <asyncio.runners.Runner object │ │
│ │ at 0x000001F739D026C0>>, main_task=<Task finished name=‘Task-1’ coro=<run_agent() │ │
│ │ done, defined at │ │
│ │ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:28> │ │
│ │ exception=KeyError(‘config’)>) │ │
│ │ prompt = None │ │
│ │ servers = [{‘type’: ‘stdio’, ‘command’: ‘npx’, ‘args’: [‘@playwright/mcp@latest’]}] │ │
│ │ sigint_registered_in_loop = False │ │
│ │ tb_str = ‘Traceback (most recent call last):\n File │ │
│ │ “C:\Python312\Lib\site-packag’+346 │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:134 in run_agent │
│ │
│ 131 │ │ │ servers=servers, │
│ 132 │ │ │ prompt=prompt, │
│ 133 │ │ ) as agent: │
│ ❱ 134 │ │ │ await agent.load_tools() │
│ 135 │ │ │ print(f”[bold blue]Agent loaded with {len(agent.available_tools)} tools:[/bo │
│ 136 │ │ │ for t in agent.available_tools: │
│ 137 │ │ │ │ print(f"[blue] • {t.function.name}[/blue]") │
│ │
│ ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │
│ │ abort_event = <asyncio.locks.Event object at 0x000001F739DE9340 [unset]> │ │
│ │ agent = <huggingface_hub.inference._mcp.agent.Agent object at 0x000001F739DE93A0> │ │
│ │ agent_path = ‘agent.json’ │ │
│ │ config = { │ │
│ │ │ ‘model’: ‘Qwen/Qwen2.5-72B-Instruct’, │ │
│ │ │ ‘provider’: ‘nebius’, │ │
│ │ │ ‘servers’: [ │ │
│ │ │ │ {‘type’: ‘stdio’, ‘command’: ‘npx’, ‘args’: [‘@playwright/mcp@latest’]} │ │
│ │ │ ] │ │
│ │ } │ │
│ │ exit_event = <asyncio.locks.Event object at 0x000001F739DE9370 [unset]> │ │
│ │ first_sigint = True │ │
│ │ inputs = │ │
│ │ loop = │ │
│ │ original_sigint_handler = functools.partial(<bound method Runner._on_sigint of <asyncio.runners.Runner object │ │
│ │ at 0x000001F739D026C0>>, main_task=<Task finished name=‘Task-1’ coro=<run_agent() │ │
│ │ done, defined at │ │
│ │ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\cli.py:28> │ │
│ │ exception=KeyError(‘config’)>) │ │
│ │ prompt = None │ │
│ │ servers = [{‘type’: ‘stdio’, ‘command’: ‘npx’, ‘args’: [‘@playwright/mcp@latest’]}] │ │
│ │ sigint_registered_in_loop = False │ │
│ │ tb_str = ‘Traceback (most recent call last):\n File │ │
│ │ "C:\Python312\Lib\site-packag’+346 │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ C:\Python312\Lib\site-packages\huggingface_hub\inference_mcp\agent.py:57 in load_tools │
│ │
│ 54 │ │
│ 55 │ async def load_tools(self) → None: │
│ 56 │ │ for cfg in self._servers_cfg: │
│ ❱ 57 │ │ │ await self.add_mcp_server(cfg[“type”], **cfg[“config”]) │
│ 58 │ │
│ 59 │ async def run( │
│ 60 │ │ self, │
│ │
│ ╭───────────────────────────────────── locals ─────────────────────────────────────╮ │
│ │ cfg = {‘type’: ‘stdio’, ‘command’: ‘npx’, ‘args’: [‘@playwright/mcp@latest’]} │ │
│ │ self = <huggingface_hub.inference._mcp.agent.Agent object at 0x000001F739DE93A0> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
KeyError: ‘config’

1 Like

The probable cause seems to be this.


Root cause

You are running a Python Tiny Agents runner (traceback is in huggingface_hub ... inference_mcp ... cli.py). That runner iterates servers and does cfg["config"]. Your agent.json has a server entry with no "config" key, so it crashes with KeyError: 'config'.

This happens because the HF course and HF JS docs show a different (flat) server shape that is valid for the JS CLI (npx @huggingface/tiny-agents ...), while some Python runners expect a nested "config" object.

You can see the course page showing a flat server entry (command and args at the top level) and telling you to run with npx @huggingface/tiny-agents run agent.json. (Hugging Face)
You can also see the JS tiny-agents docs showing the same flat shape and the same npx @huggingface/tiny-agents run ... usage. (Hugging Face)

Separately, you can see configs that use the nested "config" shape in the wild (example: AMD’s guide uses "config": {"command": "...npx.cmd", "args": ["-y", ...]}) while still running tiny-agents run. (AMD)


Why there are two shapes (background)

Shape A. “Flat server object” (common in JS tiny-agents examples)

Used in:

  • HF MCP course “Tiny Agents MCP Client in the Command Line” example JSON. (Hugging Face)
  • HF JS tiny-agents docs example JSON. (Hugging Face)

Looks like:

{
  "type": "stdio",
  "command": "npx",
  "args": ["@playwright/mcp@latest"]
}

Shape B. “Nested config” (some Python runners and other guides)

Used in:

  • AMD guide example JSON (explicitly shows config nesting and Windows npx.cmd). (AMD)
  • HF Hub issue reports also discuss nested config being passed around and causing KeyErrors depending on the library version. (GitHub)

Looks like:

{
  "type": "stdio",
  "config": {
    "command": "npx",
    "args": ["@playwright/mcp@latest"]
  }
}

Why it’s confusing

The course page is explicitly teaching the JS CLI path (npx @huggingface/tiny-agents ...) and shows the flat JSON. (Hugging Face)
But your command tiny-agents run agent.json is executing a Python-installed command (based on the traceback path). That Python runner expects the nested key and fails.


Fix 1 (most direct): add "config" in your agent.json

Replace your server entry:

{ "type": "stdio", "command": "npx", "args": ["@playwright/mcp@latest"] }

With:

{
  "type": "stdio",
  "config": {
    "command": "npx",
    "args": ["@playwright/mcp@latest"]
  }
}

Full example matching your model/provider:

{
  "model": "Qwen/Qwen2.5-72B-Instruct",
  "provider": "nebius",
  "servers": [
    {
      "type": "stdio",
      "config": {
        "command": "npx",
        "args": ["@playwright/mcp@latest"]
      }
    }
  ]
}

Windows-hardening tweaks (strongly recommended)

On Windows, it is often more reliable to point to npx.cmd explicitly and disable prompts:

AMD’s working Windows example uses a full path to npx.cmd and passes -y in args. (AMD)

So you can do:

{
  "type": "stdio",
  "config": {
    "command": "C:\\Program Files\\nodejs\\npx.cmd",
    "args": ["-y", "@playwright/mcp@latest"]
  }
}

Fix 2 (keep the course JSON): run the JS CLI exactly as the course says

If you want to keep the flat server JSON as shown in the course, run it the way the course instructs:

  • npx @huggingface/tiny-agents run agent.json (Hugging Face)

This matches the flat schema shown in both the course and the JS docs. (Hugging Face)


Version drift is real (why you see KeyErrors at all)

Hugging Face has a known class of issues where the runner expects one structure but receives the other. Example: a Hub issue report shows KeyError: 'command' when a nested config dict is provided but the underlying code expects params["command"] at the top level. (GitHub)

Your case is the mirror image: the runner expects cfg["config"], but your JSON is flat.

This is why two people can “follow the docs” and still hit different KeyErrors.


Quick sanity checks (so you stop guessing)

1) Confirm which tiny-agents you are running

Your traceback indicates Python, but you can confirm by checking where the executable resolves:

  • Windows: where tiny-agents

If it points into a Python Scripts directory, you are on the Python runner.

2) Confirm you installed the course’s prerequisites

The course explicitly says you need Node tooling (npm and npx) even if you are using Python, and it shows installing huggingface_hub[mcp] with a minimum version. (Hugging Face)


High-quality references (keep these open while debugging)


Summary

  • Your error is exactly: runner expects servers[*].config, your JSON does not have it.
  • Fix path A: nest command and args under "config".
  • Fix path B: keep flat JSON and run with npx @huggingface/tiny-agents ... as the course shows.
  • On Windows, prefer npx.cmd and -y for reliability.