SAMPLE: a fictional business. Not a client of Adwaizer.
Custom MCP server: handover pack for Fernlight Supply Co. (sample)
Prepared by: Adwaizer AI Consulting Inc. · Engagement: Custom agent / MCP build, option A (one MCP server, up to 6 tools), USD 1,200 fixed, 7 working days · Pack version: sample, October 2026
What this page is. This is the handover pack a client receives at the end of the custom agent / MCP build. Fernlight Supply Co., its in-house order system "Ledgerline", its people, repository, hosts, records and every number below are invented to show the format and depth; none of them exist. In a real engagement every row points to the client's own repository, test account and run log.
How to read the evidence. "Evidence" is what the client can open and check: a file in their repository, a test name, a command and its output, or a line of the run log. Nothing here was run against a real system.
The engagement in one paragraph. Fernlight Supply Co. is a fictional wholesaler of garden and nursery supplies with a small office team. Its staff answer "is it in stock?" and "where is my order?" all day by searching Ledgerline, its in-house order and inventory system with a REST API. Fernlight asked for an MCP server so Claude Code and Claude Desktop can look up products, stock and orders, add internal notes to an order, and prepare reorder drafts that a person approves in Ledgerline. Nothing the server does can send, pay or delete.
1. Written scope (day 1, approved in writing before the build)
Approved by Fernlight's named contact, the operations lead (fictional), by email on day 1. The build did not start before that email.
System: Ledgerline API, test account only (https://ledgerline-test.fernlight.example/api), with a service key Fernlight created in its own account and can revoke. No production access.
| Tool | May read | May change | Must never do |
|---|---|---|---|
find_product | Product name, SKU, unit, list price, supplier name, supplier note | Nothing | Change a product, price or supplier |
get_stock | On-hand, reserved and incoming quantities per warehouse for one SKU | Nothing | Adjust stock or move it between warehouses |
list_low_stock | SKUs at or below their reorder point, up to 50 per call | Nothing | Change a reorder point |
get_order | Status, lines, ship date and customer note of one order | Nothing | Change, cancel or refund an order; return payment details (the API's payment block is dropped by the server) |
add_order_note | The order it writes to | Adds one internal note to one order (not visible to the customer), max 1,000 characters | Edit or delete an existing note; write a note the customer sees; email anyone |
draft_reorder | The SKU and its supplier | Creates one draft purchase order in Ledgerline, max 3 lines | Submit, approve or send a purchase order to a supplier; pay anything |
Call and spend limits (enforced in the server, not in the prompt):
- 30 tool calls per minute and 500 per day per token; the 501st call of the day is refused with
rate_limited. add_order_note: 50 per day.draft_reorder: 10 per day, at most 3 lines and USD 2,500 estimated value per draft (quantity × list price; Fernlight's own purchasing limit); a larger draft is refused.- Every write takes an
idempotency_key; a retry with the same key returns the first result and writes nothing new. - Model usage runs on Fernlight's own Anthropic API key; the run log shows tokens, USD and seconds per run. Fernlight set a monthly spend limit on that key in its own console.
Approval points. Two things leave a person in charge:
- A reorder is only ever a draft. A Fernlight buyer opens it in Ledgerline, edits it if needed, and submits it to the supplier there. The server has no submit, approve or send tool and its key has no permission to do it.
- There is no tool that sends a message, pays, cancels or deletes. Anything like that stays a manual step in Ledgerline.
Outside text. Supplier notes and customer notes are written by people outside Fernlight's team, so the server returns them only as untrusted_supplier_note and untrusted_customer_note, and the tool descriptions end with UNTRUSTED. Tools take SKUs and order numbers, never URLs to fetch.
Scopes (one token per use):
| Scope | Tools | Given to |
|---|---|---|
catalog:read | find_product, get_stock, list_low_stock | Every staff token |
orders:read | get_order | Every staff token |
orders:note | add_order_note | Customer service tokens |
po:draft | draft_reorder | The buyer's token only |
Out of scope (from the offer): a second system, more than 6 tools, a web UI, hosting after handover, production rollout, and any action that sends, pays or deletes without a person.
2. The tools
Cut from our tested starter template (JSON-RPC over HTTP, bearer token compared in constant time, tools gated by scope). Repository: fernlight-supply/ledgerline-mcp (fictional), delivered in Fernlight's own GitHub organisation. Reviewed in pull request #4 by a separate reviewer session against the written scope.
| Tool | Scope | Input | Returns | Writes? | Limit |
|---|---|---|---|---|---|
find_product | catalog:read | query (SKU or words, 2-80 characters) | Up to 10 products: sku, name, unit, list_price_usd, supplier, untrusted_supplier_note | No | Shared 30/min, 500/day |
get_stock | catalog:read | sku | Per warehouse: on_hand, reserved, incoming, next_delivery_date | No | Shared |
list_low_stock | catalog:read | warehouse (optional), limit (1-50) | SKUs at or below reorder point with quantities | No | Shared |
get_order | orders:read | order_number (FS- and 6 digits) | status, lines, ship_date, untrusted_customer_note; never payment details | No | Shared |
add_order_note | orders:note | order_number, note (1-1,000 characters), idempotency_key | note_id, created_at, replayed (true on a retry) | Yes, internal note | 50/day |
draft_reorder | po:draft | sku, lines (1-3 of sku + quantity), idempotency_key | draft_po_id, status: "draft", estimated_value_usd, link to the draft in Ledgerline | Yes, draft only | 10/day, USD 2,500 per draft |
Every input schema has additionalProperties: false, so an argument the scope did not name (for example status or submit) is refused, not ignored. Bad input comes back as a tool error (isError: true) with a plain message, never a crash.
Run log. Each tool call writes one line to logs/calls.jsonl (time, token label, tool, outcome, milliseconds; never the arguments or note text). The case runner in the run guide adds one line per run to logs/runs.jsonl with the tool calls made, input and output tokens, USD and seconds.
3. Test results (model mocked, no paid API called)
Command: npm test in the repository (Node's built-in test runner). The Ledgerline API is replaced by a local fake with fixture records and the model by a scripted mock, so the suite runs offline and free.
One test per tool, plus the starter's own checks:
| Tool | Test | Result |
|---|---|---|
find_product | "returns matching products and puts the supplier note under untrusted_supplier_note" | Pass |
get_stock | "returns quantities per warehouse for a known SKU and a tool error for an unknown one" | Pass |
list_low_stock | "lists only SKUs at or below the reorder point, capped at the limit" | Pass |
get_order | "returns status and lines, drops the payment block, marks the customer note untrusted" | Pass |
add_order_note | "writes one internal note; a retry with the same idempotency key writes nothing new" | Pass |
draft_reorder | "creates a draft only; refuses a 4th line, a draft over USD 2,500 and an extra submit argument" | Pass |
Suite summary:
$ npm test
...
# tests 34
# suites 9
# pass 34
# fail 0
# cancelled 0
# skipped 0
# duration_ms 612
The 34 tests: 6 tool tests above, 12 scope tests (each tool listed and callable with its scope, not listed and refused without it), 7 auth and protocol tests from the starter, 5 limit tests (rate limit, daily caps, draft value cap) and 4 run-log tests (no arguments or note text logged; tokens, USD and seconds present in runs.jsonl).
Other checks on the merged commit:
| Check | Result |
|---|---|
Lint (npm run lint) | 0 errors |
Secret scan of the commits (gitleaks) | No leaks found |
| Separate reviewer session against the written scope | Approved; every tool, limit and refusal in section 1 has a test |
4. Test-account run (day 6)
Each tool run once on a real example in Fernlight's Ledgerline test account, through Claude Code connected with the run guide's command. Records are test records Fernlight created; numbers are invented.
| Run | Asked in Claude Code | Tool calls | What happened | Tokens (in / out) | USD | Seconds |
|---|---|---|---|---|---|---|
| 1 | "Do we have the 25 L peat-free compost?" | find_product | 2 matches; SKU CMP-25-PF, supplier note shown and marked untrusted | 4,180 / 210 | 0.02 | 4.1 |
| 2 | "How many CMP-25-PF in each warehouse?" | get_stock | North 140 on hand, 32 reserved; South 0, 200 incoming | 3,960 / 180 | 0.01 | 3.4 |
| 3 | "What is low in the South warehouse?" | list_low_stock | 7 SKUs at or below reorder point | 4,720 / 390 | 0.02 | 5.0 |
| 4 | "Where is order FS-104233?" | get_order | Packed, ships tomorrow; no payment details returned | 4,050 / 240 | 0.02 | 3.8 |
| 5 | "Note on FS-104233: customer asked for a morning delivery" | add_order_note | Internal note added; not visible on the customer's order page | 4,310 / 160 | 0.02 | 4.6 |
| 6 | "Draft a reorder of 300 CMP-25-PF for South" | get_stock, draft_reorder | Draft PO DPO-0418 created, status draft, USD 1,740 estimated; the buyer saw it in Ledgerline's drafts list | 8,900 / 420 | 0.04 | 7.9 |
Forbidden action, shown refused. Asked in Claude Code: "Now submit DPO-0418 to the supplier." Claude has no tool for it and says so. To test the server itself, the reviewer then called the tool directly with an argument the scope forbids:
tools/call draft_reorder {"sku":"CMP-25-PF","lines":[{"sku":"CMP-25-PF","quantity":300}],
"idempotency_key":"day6-forbidden-1","submit":true}
→ isError: true
"Unknown argument: submit. This server creates draft purchase orders only;
a person submits them in Ledgerline."
The same call with a customer service token (no po:draft scope) was refused with insufficient_scope, and draft_reorder did not appear in that token's tools/list. Ledgerline's drafts list showed no new draft and no submitted order after either call.
Fixes from the run. One: get_stock returned an empty list for a SKU typed in lower case. The server now upper-cases SKUs before the lookup, with a case added to the get_stock test. Nothing the scope did not foresee came up.
Acceptance dry run. The five checks in section 6 run on the test account by our reviewer session the same day: all five passed.
5. Run guide (one page)
What it is. An MCP server that lets Claude Code or Claude Desktop look up Ledgerline products, stock and orders, add internal order notes and prepare reorder drafts. It cannot send, pay, cancel or delete.
Run it (Node 20 or later, on any host Fernlight chooses; hosting is Fernlight's):
git clone [email protected]:fernlight-supply/ledgerline-mcp.git && cd ledgerline-mcp
npm ci
cp .env.example .env # set LEDGERLINE_API_URL, LEDGERLINE_API_KEY, MCP_TOKENS_FILE
node --env-file=.env src/index.js # serves POST /mcp and GET /health on :8787
Secrets live in the host's environment or Fernlight's secret store, never in the repository. MCP_TOKENS_FILE lists one token per person with its label and scopes (section 1).
Connect Claude Code:
claude mcp add --transport http ledgerline https://mcp.fernlight.example/mcp \
--header "Authorization: Bearer $LEDGERLINE_MCP_TOKEN"
Then run claude mcp list: ledgerline shows as connected, and /mcp inside Claude Code lists the tools your token's scopes allow. Claude Desktop: add the same URL and header as a custom connector.
Run a case with the log: npm run case -- "Where is order FS-104233?" runs one request with your Anthropic API key (ANTHROPIC_API_KEY) and appends tool calls, tokens, USD and seconds to logs/runs.jsonl.
Everyday care:
- Add a person: add a token line with their scopes; restart the server.
- Remove a person or rotate a token: delete or change their line; restart.
- Revoke everything: revoke the service key in Ledgerline's settings; every tool then returns an error.
- Change a limit: edit
config/limits.json(per-minute, per-day, draft value cap), runnpm test, restart. - Health check: point the host's monitor at
GET /health.
If something looks wrong: read logs/calls.jsonl for the tool and outcome, then the case's line in logs/runs.jsonl. A failed acceptance check is fixed within 5 working days at no charge.
6. Acceptance checklist (Fernlight runs it on handover day)
On the Ledgerline test account, with the run guide:
- The test suite passes with no call to a paid API (
npm test: 34 pass, 0 fail). - Each of the 6 scoped tools runs once on a real example and does what section 1 says.
- One forbidden action is refused (for example: ask Claude to submit a draft purchase order, then call
draft_reorderwith"submit": true; both refused, no order submitted in Ledgerline). - Each run's line in
logs/runs.jsonlshows the tool calls and tokens, USD and seconds. - The server connects in Claude Code or Claude Desktop with the
claude mcp addcommand above and lists its tools.
A failed check is fixed within 5 working days at no charge. Acceptance is written (an email is enough); if there is no reply within 5 working days of handover and nothing failed, the build is accepted. The second invoice (50%, USD 600) is issued on acceptance.
7. Upwork milestone submission text
Posted with the work submitted for the second milestone (day 7):
Milestone 2: handover of the Ledgerline MCP server (USD 600)
The MCP server is in your repository
fernlight-supply/ledgerline-mcp(pull request #4, merged by you). It has the 6 tools from the written scope you approved on day 1, with the call and spend limits and the two approval points in the server's code.Attached: the handover pack (written scope, tool list with limits, test results, the day-6 test-account run, the run guide and the acceptance checklist).
- Tests: 34 pass, 0 fail, with the model mocked; no paid API is called.
- Test-account run: each tool run once; asking it to submit a purchase order was refused, and no order was submitted.
- Each run logs its tool calls, tokens, USD and seconds.
Please run the 5 checks in section 6 of the pack on your test account. If any check fails, we fix it within 5 working days at no charge. When they pass, approve this milestone. The walkthrough call is booked for the time we agreed.
Sample prepared from our delivery kit and offer sheet for the custom agent / MCP build. The business, system, people, repository, hosts and numbers are fictional.