Skip to main content
Glama

TutorLoop

npm CI license

An MCP server that lets a chat assistant drop a graded coding exercise into the conversation. Lesson on the left, editor on the right, Run and Submit. Hidden tests decide pass/fail, and a failed attempt goes back into the chat with one click — so the explanation you get is about your code.

Python runs on Pyodide, JavaScript and TypeScript in a sandboxed worker, SQL on SQLite. Everything executes in the browser: no accounts, no backend, no per-student compute.

MIT licensed.


What makes it different

The editor is a commodity. The grading loop is the point:

  1. The model authors a hidden test suite per exercise, and the widget runs it against the submission.

  2. You get a pass/fail verdict, not just stdout.

  3. On failure, one button posts your exact code and the exact error back into the chat, where the tutor that set the exercise is still sitting.

There is no curriculum. Every exercise is generated on demand, for whatever you asked to learn.

Related MCP server: E2B MCP Server

Install

Requires Node 20+.

Claude Code

claude mcp add tutorloop -- npx -y tutorloop

Claude Desktop — Settings → Developer → Edit Config, then add:

{
  "mcpServers": {
    "tutorloop": {
      "command": "npx",
      "args": ["-y", "tutorloop"]
    }
  }
}

Restart Claude Desktop afterwards — quit it from the tray, since closing the window isn't enough.

git clone https://github.com/1hsanullah/tutorloop.git
cd tutorloop
npm install          # builds dist/ via the prepare script

Then point the config at the built file instead of npx:

claude mcp add tutorloop -- node /absolute/path/to/tutorloop/dist/server.js
{
  "mcpServers": {
    "tutorloop": {
      "command": "node",
      "args": ["/absolute/path/to/tutorloop/dist/server.js"]
    }
  }
}

Then ask for something: "teach me Python list comprehensions", "quiz me on Array.reduce", "drill me on SQL GROUP BY", "test me on narrowing TypeScript unions".

The tool

One tool, render_exercise. The model writes the lesson and the tests:

{
  "language": "python",                  // "python" | "javascript" | "typescript" | "sql"
  "title": "f-strings: formatting numbers",
  "lesson_md": "The concept plus one worked example. Short.",
  "task_md": "What to implement, including the exact function name.",
  "starter_code": "def format_price(x):\n    # your code here\n    pass\n",
  "tests": "assert format_price(3.14159) == \"$3.14\"\nassert format_price(10) == \"$10.00\"\n",
  "hints": ["Look at the :.2f format spec.", "..."],
  "solution": "def format_price(x):\n    return f\"${x:.2f}\"\n"
}

The form of tests follows the language:

Language

How it is graded

python

Plain assert statements, run in the same scope as the learner's code

javascript, typescript

assert(cond, msg), assertEquals(a, b), assertDeepEquals(a, b), same scope

sql

A reference query; the learner's result set is compared to its result set

SQL exercises also take a seed — the CREATE TABLEs and INSERTs that run first. The widget shows the learner the resulting tables, columns and row counts, so the schema is not a secret; the reference query is.

Two decisions worth knowing about SQL. Grading compares results, not query text, so any correct formulation passes. And the reference is SQL you would accept, not literal expected rows, because writing a query is something a model does reliably while hand-computing the rows it should return is exactly where model-authored tests go wrong and mark a correct learner wrong. Row order counts only if the reference has an ORDER BY; a first line of -- ordered or -- unordered overrides that. An INSERT/UPDATE/DELETE exercise is graded on the resulting table contents instead of a result set.

TypeScript is erased to JavaScript before running, with ts-blank-space, which replaces types with spaces rather than deleting them — so a runtime error's line number still points at the learner's own TypeScript, with no source map involved. Erasable TypeScript only, the same constraint Node's --experimental-strip-types has: no enum, namespace or constructor parameter properties. The widget explains that rather than throwing if a model uses one.

Hosts that cannot render widgets (terminal clients, older hosts) get the same exercise as text instead of an error — without the hidden tests.

The cheating problem

The answer key is one message away in the same chat. Mitigations, in order of value:

  • solution stays hidden until three failed submissions, then a "show solution" button appears.

  • Hints escalate: the first is free, each later one unlocks after another failed attempt.

  • The tool description tells the model to explain the learner's mistake rather than hand over the answer.

A determined learner defeats all of this. That is their time to spend.

How it fits together

host (Claude, ChatGPT, Goose, VS Code)
   │  tools/call: render_exercise({lesson, starter_code, tests, ...})
   ▼
MCP server (Node, stdio)                       src/server.ts
   │  text result + structuredContent + _meta.ui.resourceUri → ui://lesson/exercise.html
   ▼
widget (sandboxed iframe, one self-contained HTML file)
   ├── CodeMirror 6            editor, bundled — no CDN
   ├── module worker           Pyodide (Python), SQLite (SQL), plain JS (JS/TS)
   └── MCP Apps bridge         delivers the exercise, posts failures back into the chat

The server is a thin content pipe: no curriculum, no state, no execution. Everything interesting is client-side. Only the language runtimes are fetched at run time — Pyodide (~10 MB), SQLite (~1.5 MB) and the TypeScript parser (~1 MB compressed) — all from cdn.jsdelivr.net, which the server declares in the resource's CSP metadata, and each only when a learner opens an exercise that needs it. Everything else, CodeMirror included, is inlined in the widget.

The bridge is @modelcontextprotocol/ext-apps (MCP Apps, SEP-1865) with a window.openai fallback so the same bundle works in ChatGPT.

Development

npm run build       # dist/widget.html + dist/server.js
npm run dev         # rebuild on change
npm run dev:serve   # http://localhost:5173/dev/harness.html
npm test            # unit tests + a real stdio client against the built server
npm run typecheck

dev/harness.html is a working MCP Apps host: it speaks the real protocol over postMessage, delivers a sample exercise, and shows the messages the widget posts back into the "chat" in a column on the right. What works there is what works in a chat client, minus the host's CSP.

Source layout:

Path

What lives there

src/server.ts

MCP server: one tool, one resource

src/exercise.ts

The tool schema and the text-only fallback

widget/main.ts

UI, verdicts, hint and solution gating, the post-back message

widget/runner.ts

Dispatch, the JS/TS grader, first-failure extraction

widget/sql-engine.ts

SQLite, result-set comparison, the schema panel's data

widget/typescript.ts

Type erasure, and what to say when it is not possible

widget/python-driver.ts

The Python half of the grader (tracebacks, tracing deadline)

widget/runner-client.ts

Worker lifecycle, watchdog, main-thread fallback

widget/host.ts

MCP Apps / window.openai bridge

Known limits

  • Pyodide is a ~10 MB cold start. It loads lazily on the first Run and stays warm for the life of the widget instance. The widget starts warming it as soon as an exercise arrives.

  • Pyodide 314 refuses classic workers, so the runner uses a module worker and imports pyodide.mjs. A host that blocks blob: workers falls back to the main thread, where grading still works but a runaway loop can only be stopped by Python's own tracing deadline.

  • Runaway code is stopped two ways: a tracing deadline inside Python (10 s, raised in the learner's own frame, so the worker survives), and a watchdog that terminates and replaces the worker if that fails.

  • Model-authored tests are sometimes wrong. A learner will occasionally be marked wrong while being right. The escape hatch is arguing with the tutor in the chat, which is why the failure message is one click from the conversation and one click from the clipboard.

  • The widget is ~920 kB as a single HTML file, nearly all of it CodeMirror. That is well inside what hosts accept over stdio, and it buys an editor that needs no CDN.

  • SQL grading uses the first result set a script produces, and compares table contents when there is none. A script with two SELECTs is graded on the first.

  • A host CSP without wasm-unsafe-eval would block Pyodide entirely. The widget says so in the pane rather than failing silently.

  • The tool declares no outputSchema, deliberately. The MCP TypeScript SDK stamps schemas it generates from zod as draft-07, and hosts compile a tool's output schema with a 2020-12-only validator — which rejects the whole tool ("declares unsupported dialect"). Input schemas are not compiled by hosts, so only the output schema was affected. structuredContent is still returned.

Answers to the spec's open questions

  • Does the widget persist across turns? One widget instance per render_exercise call. Within an instance the Pyodide worker stays warm across runs and submissions; a new exercise means a new instance and a new cold start.

  • Can the widget read conversation context? No. Everything arrives as tool arguments (mirrored into structuredContent), which is why the model must author the whole exercise up front.

  • Is there a size ceiling that rules out shipping Pyodide from the server? Not proven, but at ~10 MB of wasm it would be reckless over stdio. Pyodide stays on the CDN; only CodeMirror and the app are inlined.

Testing note

npm test covers the markdown renderer, the tool schema, the server protocol over real stdio, and the JavaScript, TypeScript and SQL graders. The last two only run offline because their engines are imported through a URL the tests point at the local install, so nothing reaches the network.

The Python grader and all UI behaviour need a browser, and were verified through the dev harness: pass, failed assertion with the exact failing line, runtime error attributed to the learner's line, infinite loop stopped at the deadline, hint and solution gating, and the post-back message arriving in the host. The SQL and TypeScript engines have not yet been driven in a browser — they are loaded exactly the way Pyodide is, which is proven, but that is an argument rather than a measurement. Run the harness against the SQL and TypeScript samples before trusting them.

Available Tools

1 tool
render_exerciseRender a graded coding exerciseA
Read-only

Render a graded coding exercise as an interactive widget in the conversation.

The learner gets a lesson pane and an editor pane with Run and Submit. Submit executes your hidden tests against their code in the browser and shows a pass/fail verdict. On failure they can push their exact code and the exact error back into this conversation with one click — that is the point of this tool, so write tests whose failures are informative.

Call this when someone asks to learn, practise or be quizzed on Python, JavaScript, TypeScript or SQL. Author everything yourself: there is no curriculum behind this tool.

Rules for good exercises:

  • One concept, one answer, solvable in a few minutes.

  • 'task_md' must state exactly what is wanted: the function and signature the tests call, or for SQL which columns come back and whether row order matters.

  • 'starter_code' contains the skeleton and a placeholder, never the answer.

  • 'tests' are hidden until submission, and their form differs by language — see the field description. Test only what 'task_md' actually asked for; a test the learner could not have anticipated is a bug, not a difficulty.

  • SQL exercises also need 'seed'. The learner sees the schema, not your reference query.

  • Hints escalate: first a nudge, last one close to the answer.

When the learner is stuck or posts a failed attempt back into the chat, explain what THEIR code did wrong and point at the next hint. Do not hand over the solution on request; that is what the gated "show solution" button in the widget is for. If they insist after several genuine attempts, walk them through it rather than pasting it.

ParametersJSON Schema
NameRequiredDescriptionDefault
seedNoSQL only, and required for sql: CREATE TABLE statements plus INSERTs, run before anything else on a fresh in-memory database. Keep it small — a handful of rows chosen so a wrong query gives a visibly wrong answer, including at least one edge case (a NULL, a tie, an empty group). The widget shows the learner the resulting table names, columns and row counts, so this is not hidden; the reference answer in 'tests' is.
hintsNoEscalating hints, nudge first and near-answer last. Revealed one at a time: the first is free, each later one unlocks after another failed submission.
testsYesThe hidden grader, never shown before submission. Its form depends on the language. python: plain `assert` statements, at least three, one an edge case. javascript/typescript: `assert(cond, msg)`, `assertEquals(actual, expected)` and `assertDeepEquals(actual, expected)` are in scope; at least three assertions. Tests run in the same scope as the learner's code, so they can call whatever it defines. TypeScript tests may be typed; the types are erased before running. sql: a *reference answer written as SQL* — not expected rows. It runs against its own copy of the seeded database and the learner's result set is compared to it, so write the query you would accept as correct. Row order is enforced only if the reference has an ORDER BY; force it either way with a first line of `-- ordered` or `-- unordered`. For an INSERT/UPDATE/DELETE exercise, write the equivalent statements and the resulting table contents are compared instead.
titleYesShort exercise title, e.g. 'f-strings: formatting numbers'.
task_mdYesMarkdown. Exactly what the learner must produce. For python/javascript/typescript, name the required function and its signature. For sql, say which columns to return, in what order, and whether row order matters.
languageYesRuntime for the exercise. 'python' runs on Pyodide; 'javascript' and 'typescript' in a sandboxed worker; 'sql' on SQLite. All four execute in the learner's browser.
solutionNoReference solution. Kept hidden by the widget until three failed submissions.
lesson_mdYesMarkdown. The concept plus one worked example. Keep it short — a screenful, not a chapter.
starter_codeYesWhat the editor opens with. Include the signature and a '# your code here' placeholder, never the answer. For sql, a comment and the skeleton of a statement (e.g. 'SELECT ... FROM orders').

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description is highly transparent about the tool's behavior and implications. It explains the submission flow ('Submit executes your hidden tests against their code in the browser and shows a pass/fail verdict'), the failure recovery mechanism (one-click push of code and error back into the conversation), and the purpose of the tests ('write tests whose failures are informative'). It also discloses the policy around solutions: 'Do not hand over the solution on request; that is what the gated "show solution" button in the widget is for.' This enriches the readOnlyHint annotation by clarifying what side effects occur (none external) and what the widget does. No contradiction with annotations; in fact, it reinforces that the tool does not mutate external state.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average, but it is well-structured with a clear opening purpose, followed by contextual usage, then a bulleted list of rules for good exercises, and behavioral guidance. The organization makes it easy to scan, and the essential action ('Render a graded coding exercise') is front-loaded. While some sentences could be tightened (e.g., merging overlapping rules), the length is justified given the complexity of the tool and the need to communicate subtle constraints. It earns a 4, falling short of 5 because it is not maximally concise.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (9 parameters, language-specific behaviors, hidden tests, SQL seeding, hint escalation) and the lack of an output schema, the description is remarkably complete. It explains the widget's interactive behavior, what happens on submission, the policy on solutions, and the exact authoring requirements for each language. It also addresses edge cases like SQL row order and hidden tests. The description covers enough for an agent to call the tool correctly without needing additional context. No essential information is missing; the only minor lack is an explicit indication of what the tool returns (since there is no output schema), but the description implies the widget is returned.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already covers all parameters with descriptions (100% coverage), so the baseline is 3. The description adds meaningful value beyond the schema by providing concrete authoring rules for several parameters: e.g., 'task_md must state exactly what is wanted', 'starter_code contains the skeleton and a placeholder, never the answer', 'tests are hidden until submission' with language-specific guidance, and 'SQL exercises also need seed' with details on edge cases. These are not just restatements; they give the agent actionable instructions that the schema does not. The description also advises on hint escalation, which maps to the 'hints' parameter. This lifts the score to 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a clear, specific statement of what the tool does: 'Render a graded coding exercise as an interactive widget in the conversation.' It names the resource (graded coding exercise), the action (render as interactive widget), and the context (conversation). It goes on to describe the learner experience (lesson pane, editor pane, Run/Submit) without ambiguity. Since there are no sibling tools, differentiation is not needed, but the purpose is unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states when to call the tool: 'Call this when someone asks to learn, practise or be quizzed on Python, JavaScript, TypeScript or SQL.' It also provides clear non-usage guidance by noting 'there is no curriculum behind this tool' and advising the agent to author everything itself. While it does not name an alternative tool (since none exist), it gives enough context on triggers and expectations. A slightly higher score is withheld because it does not explicitly mention scenarios where the tool should NOT be used (e.g., when a non-supported language is requested), though the language enum implicitly covers that.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedrender_exercise

TDQS

A4.5/5.0

Scored across 1 tool

Disambiguation5/5

There is exactly one tool, so no two tools can be confused with each other. The description also clearly states the single trigger condition: learning, practicing, or being quizzed on Python, JavaScript, TypeScript, or SQL.

Naming Consistency5/5

render_exercise follows a clean verb_noun snake_case convention. With a single tool there are no inconsistent patterns or mixed naming styles to create confusion.

Tool Count3/5

A one-tool server feels thin for a name like tutorloop, but the tool is a substantial, self-contained widget that bundles exercise rendering, testing, hints, and feedback. It is borderline rather than clearly over- or under-scoped.

Completeness4/5

The tool covers the essential exercise lifecycle: presenting a task, running hidden tests, surfacing failure details, and progressively hinting. There are minor gaps around tracked progress or exercise history, but an agent can work around those through conversation.

Related MCP Connectors

Related MCP Servers