This demo assumes Demos 1–4. You need a terminal, Python 3, and Claude Code signed in to the NRT organization, and you should have run at least Demo 2 so the MCP pieces are familiar. Demo 5 is helpful but not required. If you are starting cold, begin with the setup steps and the four core demos.
Package:
Demo6_bring_your_own_project.zip
Demos 1 to 5 handed you a package that had already been prepared. Someone had already decided what the tool was called, what its arguments were, what counted as a legal value, and what the agent was allowed to do. That is the part of the work this course has been quietly doing for you.
This demo takes it away. You get a research project in a field that is probably not yours, in the state real projects are actually in — one script that works, a README that is partly out of date, a notes file, and nothing else. No schema, no server, no agent instructions.
In about forty minutes you will turn it into the thing Demo 2 handed you on a plate, using four prompts. Then you will do the same to your own code.
What you are given
heat1d/
simulate.py the analysis that works
plot_profile.py an older driver, superseded, still present
input/rod_initial.csv 101 nodes, a hot band in the middle, ends held at 20 C
output/ empty
README.md written a while ago, partly wrong
notes.txt a researcher's notes to themselves
prompts/ the four prompts, ready to paste
reference/ one worked answer — open it AFTER yours
docs/WALKTHROUGH.md the short version of this page
simulate.py solves the 1-D heat equation along a thin rod with an explicit
finite-difference scheme: temperature at each node is updated from its two
neighbours, one time step at a time.
def step(T, r):
new = T[:]
for i in range(1, len(T) - 1):
new[i] = T[i] + r * (T[i + 1] - 2.0 * T[i] + T[i - 1])
return new
You do not need to know any of that physics to do this demo. You need to know one thing about it, and the whole demo is built so that you meet it the hard way.
The trap
The explicit scheme is only numerically stable when
r = alpha * dt / dx^2 <= 0.5
Push r above 0.5 and neighbouring nodes start oscillating against each other,
and the oscillation grows every step until the numbers are meaningless.
simulate.py does not check this. It computes r, prints it, and carries on.
The run finishes. The exit code is 0. A tidy two-column CSV appears in
output/. Nothing anywhere says the contents are nonsense.
This rule is not in the code and not in the README. It is hinted at in
notes.txt — "dt cannot be too big or the numbers go crazy, there is a factor
involving dx^2 — ask Wei" — and that is the point. An agent cannot infer this
rule from the repository. A person who knows the method has to supply it.
That is your job in this demo, and it is your job in your own project.
This is the fold-artifact slide again, in a different subject. In Demo 1 the classifier returned a confident answer on an image it should have refused. Here the simulation returns a well-formed file on a configuration it should have refused. In both cases the software is behaving exactly as written. The missing piece is a person stating what "valid" means, in a place the machine can check.
Before you start
You need Python 3 and Claude Code, signed in, the same as Demo 2. Nothing else
is installed — simulate.py is standard library only.
unzip Demo6_bring_your_own_project.zip
cd Demo6_bring_your_own_project
ls -R | head -40
Run the script once by hand so you have seen it work before an agent touches it:
cd heat1d
python3 simulate.py --input input/rod_initial.csv \
--alpha 1.1e-4 --dt 0.05 --dx 0.01 --nsteps 400 \
--out output/profile.csv
cd ..
wrote output/profile.csv
nodes 101
r 0.0550
T min/max 20.000 / 68.192
Now start Claude Code from the project root:
claude --permission-mode default
Part 1 — Inspect, change nothing
Open prompts/1_inspect.md and paste it:
Review this project and tell me:
- what the main analysis workflow actually is, and which file runs it
- the required inputs and their formats
- every parameter, what it means, and its units
- what the outputs are and where they go
- anything ambiguous, contradictory, or undocumented
Do not change any files. Do not run anything yet. Where the README and the code disagree, say so and tell me which one you trust and why.
What to watch for. There are three planted disagreements. A good answer finds all three and says which source it trusts:
| The disagreement | What the README says | What is true |
|---|---|---|
| The flag name | --steps |
simulate.py takes --nsteps |
The units of alpha |
mm²/s, "copper is about 111" | the input file is in metres, so alpha must be m²/s, ≈ 1.11e-4 |
| Which file runs it | plot_profile.py |
simulate.py; notes.txt says the driver was superseded |
A weaker answer paraphrases the README back to you. That is the failure mode worth naming out loud: the agent is very willing to believe documentation.
Notice also what it should not find. Nothing in this repository states the stability condition. If the model volunteers it anyway — some will, from general knowledge of finite-difference methods — that is a bonus, not something you can rely on. Ask it: "Is that rule written down anywhere in this project?" The answer is no.
Part 2 — Propose a contract, do not build it yet
prompts/2_propose_contract.md:
Propose how to expose ONE stable workflow from this project as an agent-callable capability. Do not change the scientific computation.
Give me:
- the capability's name and a one-sentence description
- required inputs and optional parameters, with types and units
- validation rules that should be checked BEFORE anything runs
- the structure of the result it should return
- what should stay under human control and not be exposed at all
List anything you cannot decide without asking me.
Separating the design from the build is deliberate. If you ask for code immediately you get code immediately, and you never see the decisions that were made on your behalf.
This is where you supply the physics. When the proposal comes back, add the rule the project does not contain:
Add one more validation rule: the scheme is explicit, so it is only stable when
r = alpha*dt/dx^2 <= 0.5. The tool must compute r and refuse to run above 0.5, reporting the largest dt that would be legal. Always return r in the result, even when the run succeeds.
Also ask for what makes a result readable six months later:
Echo every input parameter back inside the result, so the output is self-describing.
That last line matters more than it looks. simulate.py writes a bare
two-column CSV. Next spring, nobody — including you — can tell which run
produced it.
Part 3 — Build it
prompts/3_build.md:
Now build what we agreed:
capability_card.md, a human-readable description of the toolschema.json, the machine-readable input contractserver.py, a minimal MCP server exposing the one capability, which validates every argument against the schema before running anythingCLAUDE.md, telling an agent in this project which interface to prefer.mcp.jsonso Claude Code can start the serverStandard library only. Do not modify
simulate.py— wrap it.
Wrap, do not rewrite. This is the single most important instruction in the
demo. The science stays where the scientist left it; the interface is new. If
the agent starts editing simulate.py, stop it. A wrapper you can delete is a
much safer thing to have built than a rewrite you now have to re-validate.
You should end up with roughly this:
capability_card.md what the tool is, for a human
schema.json types, units, ranges — for the machine
server.py stdio MCP server; validates, then shells out to simulate.py
CLAUDE.md "prefer the MCP tool over running the script directly"
.mcp.json how Claude Code starts the server
Sanity-check the server before trusting it, exactly as in Demo 2:
python3 server.py
It will sit there silently waiting for JSON-RPC on stdin. That is correct
behaviour, not a hang. Press Ctrl-C.
Part 4 — Test it, then try to break it
Restart Claude Code so the new server is loaded. On first launch you will see
"New MCP server found in this project" — choose "Use this MCP server".
(If you pick the other option by mistake, claude mcp reset-project-choices
lets you answer again.)
prompts/4_test_and_break.md:
- Run the baseline: alpha 1.1e-4, dt 0.05, dx 0.01, nsteps 400.
- Now make it run faster: same total time, ten times fewer steps.
- Run that same faster configuration for 200 seconds of simulated time.
- Tell me which of those three results I can publish.
Step 2 is the ordinary request a busy researcher makes, and it is the one that
matters. "Same total time, ten times fewer steps" means dt goes from 0.05 to
0.5 — which takes r from 0.055 to 0.55, just over the line.
What the raw script does, if you let it
Run these by hand and look at the last line of each:
| Run | dt | nsteps | Simulated time | r | T max |
|---|---|---|---|---|---|
| baseline | 0.05 | 400 | 20 s | 0.055 | 68.192 |
| "ten times faster" | 0.5 | 40 | 20 s | 0.55 | 68.019 ← looks fine |
| the same, run longer | 0.5 | 400 | 200 s | 0.55 | 1.0e+26 |
| correct, run longer | 0.05 | 4000 | 200 s | 0.055 | 38.552 |
Every one of those exits 0 and writes a CSV.
Row 2 is the dangerous one. The unstable configuration passes a short test. The error is growing, but forty steps is not long enough for it to become visible — 68.019 against 68.192 looks like rounding. If your acceptance test is "run it and see if the answer is about right", this configuration passes, and you ship it.
Row 3 is the same configuration, given time to show what it is. Look inside the file:
python3 - <<'EOF'
import csv
v = [float(r["T_C"]) for r in csv.DictReader(open("heat1d/output/profile.csv"))]
print(["%.1e" % x for x in v[48:52]])
EOF
['8.7e+25', '-8.6e+25', '8.6e+25', '-8.6e+25']
Alternating sign, node by node, at 10²⁵ degrees. That is the instability, and it is the only reason you can tell by eye. A subtler scheme would have given you plausible-looking garbage instead.
What your wrapper should do
If you added the rule in Part 2, the tool never gets that far:
refused before running: this configuration is numerically unstable.
r = alpha*dt/dx^2 = 0.5500, and the explicit scheme requires r <= 0.5.
With alpha=0.00011 and dx=0.01, the largest usable dt is 0.4545 s.
The simulation WOULD have run and written a CSV — of oscillating nonsense.
Note where the refusal comes from. Not from the model deciding to be careful — the model asked for exactly what you told it to ask for. The refusal comes from a rule a person wrote into the contract, enforced before any computation happened. This is the Demo 2 lesson, arrived at from the other direction: in Demo 2 you were handed a schema and shown that it holds; here you find out what it costs to write one, and what happens in the gap before you do.
And the answer to question 4: only rows 1 and 4 are publishable, and only if
someone has separately decided that 1-D, these boundary conditions, and this
alpha describe the rod you actually care about. The tool checks arithmetic. It
cannot check that you are simulating the right thing.
Compare with the reference
Now — and not before — open reference/.
cp reference/.mcp.json.example .mcp.json
cp reference/CLAUDE.md .
claude
There is no single right design, and comparing two real attempts teaches more than copying one. Three things are worth checking against your own:
- Does your validation include the stability rule? If not, your wrapper is a nicer interface to the same silent failure.
- Does your result echo its own parameters? The reference returns
r,stable,t_total_s,n_nodes,T_max,T_min,out, and every input. - Did you wrap, or did you rewrite?
simulate.pyis untouched in the reference.
Claude Code or Cowork?
Both can do this. They are good at different halves of it.
| Claude Code (terminal) | Cowork (desktop app) | |
|---|---|---|
| Where your code is | already there — it reads the folder you started it in | you connect the folder, then it works in place |
| Best at | writing server.py, running it, iterating on failures, git |
reading the project and explaining it, drafting the card and schema, writing the documentation |
| Running the MCP server you just built | yes — .mcp.json, permissions, the whole Demo 2 mechanism |
not the same way; Cowork is not the client your server registers with |
| Permission model | .claude/settings.json, the rules from Demo 3 |
approvals per action, in the app |
| Good for | Parts 3 and 4 | Parts 1 and 2 |
A reasonable division of labour: use Cowork to understand the project and agree the contract, and Claude Code to build and test it. Parts 1 and 2 are reading and judgement, and a chat interface with the folder connected is a comfortable place to do that — you can point at files, ask follow-ups, and keep the design discussion out of your terminal history. Parts 3 and 4 need a tool that can create files, run a server, and answer permission prompts, which is Claude Code's job.
If you only want to learn one, learn Claude Code first. Everything in Demos 2 to 5 assumes it.
Doing this to your own project
The four prompts are not about heat. Copy them and change the nouns.
The only lines you must write yourself are the ones in Part 2 that state what "valid" means in your field. Nobody else can write them, and an agent reading your repository will not find them.
Python. Use the prompts as they are. Replace simulate.py with your entry
point, and say so: "the workflow I want to expose is the one in
analysis/run_fit.py".
R. The wrapper is the same; only the call changes. Add to Part 3:
server.pyshould invoke the R script withRscript, pass parameters as command-line arguments, and read the result back from the CSV it writes. Do not translate the R code into Python.
If your script uses optparse or argparse in R, say so — the agent should
match the flags you already have rather than inventing new ones.
MATLAB. Same shape, one extra constraint worth stating:
Invoke MATLAB with
matlab -batch "funcname(args)", and treat startup cost as real — the tool should accept a list of cases and run them in one MATLAB session rather than starting MATLAB per call.
A cluster job. Do not make the tool wait. The shape is: one capability that submits and returns a job id immediately, and a second that reports status. Say that explicitly in Part 2, because the obvious design — block until done — will otherwise be what you get.
A notebook. Extract the one workflow into a script first, and be blunt about
it: "before anything else, extract the cells from analysis.ipynb that produce
the final figure into a single script with explicit parameters, and change no
logic." A notebook's hidden execution order is exactly the kind of ambiguity
that makes a tool contract impossible to write.
Validation rules, by way of example
Part 2 is where your expertise goes in. Some shapes it takes:
- a stability or convergence condition, like the one here
- a physical range: a rate that cannot be negative, a fraction that cannot exceed 1, a temperature below absolute zero
- units — the single most common silent error, and the reason the reference card states units for every parameter
- a sample-size floor below which the statistic is not meaningful
- an input the tool should refuse rather than guess at: a file with the wrong number of columns, a grid spacing that disagrees with the data
Every one of these is something you know and your repository does not say.
What to take away
Demos 1 to 5 showed you a prepared system working. This one shows you the preparation, which is the part you will actually have to do.
Three things survive the trip to your own project:
- The agent is excellent at the mechanical half and blind to the scientific
half. It will read your code, find your contradictions, write a clean
schema and a working server in minutes. It will not tell you that
rmust stay under 0.5, because nothing you gave it says so. - Validation before execution is worth more than a check afterwards. A run that never happened costs nothing. A run that finished, wrote a file, and was wrong costs however long it takes someone to notice.
- A short test is not evidence. The unstable configuration passed the twenty-second test. Whatever your field's equivalent of "run it longer" is, that is the test that matters.