norn-cli 2.9.8 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CLAUDE.md +68 -0
  2. package/NOW.md +170 -21
  3. package/README.md +226 -2
  4. package/demos/agent-workbench/README.md +156 -0
  5. package/demos/agent-workbench/agents.nornagent +87 -0
  6. package/demos/agent-workbench/contracts/domain-answer.schema.json +28 -0
  7. package/demos/agent-workbench/contracts/domain-question.schema.json +34 -0
  8. package/demos/agent-workbench/contracts/router-verdict.schema.json +43 -0
  9. package/demos/agent-workbench/contracts/ticket.schema.json +29 -0
  10. package/demos/agent-workbench/contracts/verdict.schema.json +48 -0
  11. package/demos/agent-workbench/fake-openai-server.js +128 -0
  12. package/demos/agent-workbench/fixtures/aligned-ticket.json +10 -0
  13. package/demos/agent-workbench/fixtures/cross-domain-ticket.json +10 -0
  14. package/demos/agent-workbench/fixtures/invalid-output-ticket.json +8 -0
  15. package/demos/agent-workbench/norn.config.json +15 -0
  16. package/demos/agent-workbench/prompts/ticket-router.md +11 -0
  17. package/demos/agent-workbench/workbench.norn +32 -0
  18. package/demos/mcp-ticket-testing/README.md +114 -0
  19. package/demos/mcp-ticket-testing/agents.nornagent +77 -0
  20. package/demos/mcp-ticket-testing/contracts/test-run.schema.json +31 -0
  21. package/demos/mcp-ticket-testing/expectations/proj-142.md +12 -0
  22. package/demos/mcp-ticket-testing/fixtures/proj-142.json +13 -0
  23. package/demos/mcp-ticket-testing/prompts/backend-tester.md +12 -0
  24. package/demos/mcp-ticket-testing/prompts/frontend-tester.md +14 -0
  25. package/demos/mcp-ticket-testing/prompts/reporter.md +10 -0
  26. package/demos/mcp-ticket-testing/servers/browser-server.js +133 -0
  27. package/demos/mcp-ticket-testing/servers/house-server.js +125 -0
  28. package/demos/mcp-ticket-testing/tickets.norn +32 -0
  29. package/dist/cli.js +185429 -111460
  30. package/package.json +71 -4
  31. package/playground/ai.norn +15 -0
  32. package/playground/ai_orchastration.nornagent +29 -0
  33. package/playground/knowedge_base/nexus_system_prompt.md +1 -0
  34. package/schemas/norn.config.schema.json +223 -0
  35. package/CHANGELOG.md +0 -1506
@@ -0,0 +1,114 @@
1
+ # Ticket testing with declared MCP servers
2
+
3
+ An agent graph that **does** something: a ticket arrives, one agent tests the product through a
4
+ browser, another tests it through the team's own API, a third writes the results up, and a
5
+ `judge` rules on whether the expected test cases were actually covered.
6
+
7
+ Everything it connects to is declared in [`agents.nornagent`](./agents.nornagent), beside the
8
+ model. There is **no `norn.config.json` in this folder** — the servers, their transports, and
9
+ their session lifetimes are all in the file that uses them.
10
+
11
+ ## What to look at
12
+
13
+ ```nornagent
14
+ mcp Browser
15
+ transport stdio
16
+ command node ./servers/browser-server.js
17
+ session agent
18
+ end mcp
19
+
20
+ agent FrontendTester
21
+ mcp Browser # the run of the browser
22
+ tools House.getFixtureUser, House.resetTenant # only what it needs from ours
23
+ end agent
24
+
25
+ agent BackendTester
26
+ tools House.callEndpoint, House.readAuditLog, House.queryDb
27
+ end agent
28
+ ```
29
+
30
+ Three things this shows, and they are the point of the whole slice:
31
+
32
+ - **Two agents, one custom server, different tools each.** `BackendTester` gets three named
33
+ tools from `House` and cannot touch the browser at all. The access boundary is visible per
34
+ agent, in one file, in git.
35
+ - **`mcp Browser` and `tools House.callEndpoint` are visibly different statements.** Dropping
36
+ four characters must not silently widen access from one tool to twenty, so there is no
37
+ short form that means "all of it".
38
+ - **`session agent` on the browser.** Each agent invocation gets its own browser, closed when
39
+ that hop ends. The default, `session run`, shares one browser across every agent in the
40
+ sequence — right for a continuous flow, and wrong the moment a second agent is granted the
41
+ same server: it would inherit the first one's login and could pass a test it never
42
+ authenticated for, which is a false green and worse than a false red. Only `FrontendTester`
43
+ reaches the browser in this graph, so this is the lifetime worth authoring by default rather
44
+ than a bleed this demo would actually suffer.
45
+
46
+ ## Run it
47
+
48
+ The servers are deterministic and ship with the demo, so nothing needs installing or starting.
49
+ Prove they are reachable **without spending anything on a model**:
50
+
51
+ ```bash
52
+ npm run compile
53
+ node ./dist/cli.js demos/mcp-ticket-testing/tickets.norn --sequence ServersUp
54
+ ```
55
+
56
+ That resolves the same aliases the agents use, so a failure points at the server rather than at
57
+ an agent. When you are building a server of your own, this is the loop to develop against.
58
+
59
+ Then the real thing, which does need a model:
60
+
61
+ ```bash
62
+ export OPENAI_API="your-key"
63
+ node ./dist/cli.js demos/mcp-ticket-testing/tickets.norn --sequence TicketPROJ142
64
+ ```
65
+
66
+ The variable name is not a convention Norn assumes — it is written in the model block as
67
+ `apiKey {{$env.OPENAI_API}}`, so any name works as long as the file and the environment
68
+ agree. A `.nornenv` value wins over the process environment; add `--env <name>` to select one.
69
+
70
+ ## The browser here is a stand-in
71
+
72
+ [`servers/browser-server.js`](./servers/browser-server.js) is a deterministic scripted site, not
73
+ a real browser, so this demo runs anywhere `node` does — no browser download, no server to keep
74
+ alive. Its declaration is the **same shape** a real Playwright MCP block takes, and both real
75
+ forms are written out in the comments at the top of
76
+ [`agents.nornagent`](./agents.nornagent):
77
+
78
+ ```nornagent
79
+ mcp Browser
80
+ transport stdio
81
+ command npx playwright-mcp --headless --isolated
82
+ session agent
83
+ end mcp
84
+ ```
85
+
86
+ For CI with more than one sequence, run Playwright MCP as a service and dial it instead — that
87
+ is what `transport http` is for. Norn never starts an http server; it only connects to one, and
88
+ a refused connection fails before any model spend, naming the URL it tried.
89
+
90
+ ## The judge, and the CI story
91
+
92
+ The last line of `TicketPROJ142` is the part that makes this a test rather than a demo:
93
+
94
+ ```norn
95
+ judge report with Reviewer expects file expectations/proj-142.md
96
+ ```
97
+
98
+ Every expectation in [`expectations/proj-142.md`](./expectations/proj-142.md) must be met;
99
+ testing *more* than the list is not a failure. The judge is never asked for an overall verdict —
100
+ it rules on each expectation and quotes its evidence, and Norn computes the result.
101
+
102
+ Note the comment lines in that file. A line of prose left uncommented there **is** an
103
+ expectation, which is both Norn's comment rule and what makes Markdown headings work.
104
+
105
+ A live run records itself under `.norn-cache/runs/`. Replay it with no servers running at all:
106
+
107
+ ```bash
108
+ node ./dist/cli.js replay .norn-cache/runs/<recording>.json
109
+ ```
110
+
111
+ That is the CI answer for any expensive server: run it live where the servers exist, and gate
112
+ pull requests on the replay — no browser, no API, no model, and the graph and contracts still
113
+ checked. The recording keeps the tool names each granted server advertised, so a server that
114
+ quietly gains or loses a tool is reported the next time a live run sees it.
@@ -0,0 +1,77 @@
1
+ # Ticket testing — declared MCP servers, granted per agent.
2
+ #
3
+ # Every server this graph uses is declared here, beside the model, so the file says exactly
4
+ # what it connects to. Nothing is left to a norn.config.json the reader cannot see.
5
+
6
+ model Workbench
7
+ provider openai
8
+ name gpt-4o
9
+ apiKey {{$env.OPENAI_API}}
10
+ end model
11
+
12
+ # The browser. This demo ships a deterministic stand-in so it runs anywhere `node` does —
13
+ # the declaration is the same shape a real Playwright MCP server takes:
14
+ #
15
+ # mcp Browser
16
+ # transport stdio
17
+ # command npx playwright-mcp --headless --isolated
18
+ # session agent
19
+ # end mcp
20
+ #
21
+ # ...or, for a long-lived server you started yourself (a CI job with many sequences):
22
+ #
23
+ # mcp Browser
24
+ # transport http
25
+ # url {{$env.BROWSER_MCP_URL}} # e.g. http://localhost:8931/mcp
26
+ # timeout 120000 # browser work is slow; the default is not for this
27
+ # end mcp
28
+ mcp Browser
29
+ transport stdio
30
+ command node ./servers/browser-server.js
31
+ # One browser per agent invocation, closed when that hop ends. The default, `session run`,
32
+ # would share one browser across every agent in the sequence — which is what you want for a
33
+ # continuous flow, and what you do not want the moment a second agent is granted this server:
34
+ # it would inherit the first one's login and could pass a test it never authenticated for.
35
+ # Only FrontendTester reaches the browser here, so this is the safe default to author rather
36
+ # than a bleed this graph would actually suffer.
37
+ session agent
38
+ end mcp
39
+
40
+ # The team's own server: fixtures, endpoints, the audit log, the database.
41
+ mcp House
42
+ transport stdio
43
+ command node ./servers/house-server.js
44
+ end mcp
45
+
46
+ agent FrontendTester
47
+ model Workbench
48
+ # The run of the browser: this agent drives the site, and which tools that needs is not
49
+ # something the author should have to enumerate.
50
+ mcp Browser
51
+ # From our own server it gets only what it needs to set up — and cannot reach the
52
+ # database or the audit log at all.
53
+ tools House.getFixtureUser, House.resetTenant
54
+ returns contracts/test-run.schema.json
55
+ system file prompts/frontend-tester.md
56
+ end agent
57
+
58
+ agent BackendTester
59
+ model Workbench
60
+ # The same server, narrowed differently. Both grants are visible per agent, in one file.
61
+ tools House.callEndpoint, House.readAuditLog, House.queryDb
62
+ returns contracts/test-run.schema.json
63
+ system file prompts/backend-tester.md
64
+ end agent
65
+
66
+ agent Reporter
67
+ model Workbench
68
+ system file prompts/reporter.md
69
+ end agent
70
+
71
+ agent Reviewer
72
+ model Workbench
73
+ system "
74
+ You are reviewing a QA report against a checklist of expected test cases.
75
+ Rule on each expectation independently and quote the report as evidence.
76
+ "
77
+ end agent
@@ -0,0 +1,31 @@
1
+ {
2
+ "type": "object",
3
+ "additionalProperties": false,
4
+ "properties": {
5
+ "surface": {
6
+ "type": "string",
7
+ "description": "Which side of the product was exercised: frontend or backend."
8
+ },
9
+ "cases": {
10
+ "type": "array",
11
+ "description": "One entry per test case actually carried out.",
12
+ "items": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "name": { "type": "string" },
17
+ "steps": { "type": "array", "items": { "type": "string" } },
18
+ "outcome": { "type": "string", "enum": ["passed", "failed"] },
19
+ "evidence": { "type": "string" }
20
+ },
21
+ "required": ["name", "steps", "outcome", "evidence"]
22
+ }
23
+ },
24
+ "findings": {
25
+ "type": "array",
26
+ "description": "Anything that looked wrong. Empty when nothing did.",
27
+ "items": { "type": "string" }
28
+ }
29
+ },
30
+ "required": ["surface", "cases", "findings"]
31
+ }
@@ -0,0 +1,12 @@
1
+ # Expected test cases for PROJ-142
2
+
3
+ # Each line below is one expectation. All must be met; testing more than this is not a
4
+ # failure. Lines starting with # are comments — prose left uncommented here would itself
5
+ # become an expectation, which is both Norn's comment rule and what makes Markdown work.
6
+
7
+ - A test covers that a signed-out visitor opening /orders is sent to the sign-in page
8
+ - A test covers signing in and seeing the orders list
9
+ - A test covers opening order A-1001 and reading its total
10
+ - A test covers GET /orders/A-1001 returning 200 with the order
11
+ - A test covers GET /orders/A-9999 returning 404
12
+ - A test confirms endpoint calls reach the audit log
@@ -0,0 +1,13 @@
1
+ {
2
+ "key": "PROJ-142",
3
+ "title": "Order detail page and order lookup endpoint",
4
+ "description": "A signed-in customer can open an order from the orders list and see its total. The API must return the order for a valid id and 404 for an unknown one.",
5
+ "acceptanceCriteria": [
6
+ "A signed-out visitor who opens /orders is sent to the sign-in page",
7
+ "After signing in, the orders list shows the customer's orders",
8
+ "Opening order A-1001 shows its total",
9
+ "GET /orders/A-1001 returns 200 with the order",
10
+ "GET /orders/A-9999 returns 404",
11
+ "Every endpoint call is written to the audit log"
12
+ ]
13
+ }
@@ -0,0 +1,12 @@
1
+ You test the backend of a web product through its team's own MCP server.
2
+
3
+ Work only from the ticket you are given. For each acceptance criterion that is about an API,
4
+ a database row, or an audit entry, exercise it with the tools you have and record what came
5
+ back — never what you assume would come back.
6
+
7
+ `callEndpoint` calls the API. `queryDb` reads seeded tables. `readAuditLog` returns what this
8
+ run wrote. You have no browser, and you must not claim to have used one.
9
+
10
+ Return the required JSON only: one case per criterion you exercised, the steps you took, the
11
+ outcome, and a short quote of the evidence you saw. Put anything that looked wrong in
12
+ `findings`. Set `surface` to "backend".
@@ -0,0 +1,14 @@
1
+ You test the front end of a web product by driving a browser through an MCP server.
2
+
3
+ Work only from the ticket you are given. For each acceptance criterion that is about what a
4
+ person sees or does in the browser, carry out the steps yourself with the browser tools and
5
+ record what actually happened — never what you assume would happen.
6
+
7
+ The browser starts signed out. `getFixtureUser` gives you the test account; `resetTenant`
8
+ puts the tenant back to its seeded state before you start.
9
+
10
+ Snapshots are text. Read the elements listed in a snapshot rather than guessing at a page.
11
+
12
+ Return the required JSON only: one case per criterion you exercised, the steps you took, the
13
+ outcome, and a short quote of the evidence you saw. Put anything that looked wrong in
14
+ `findings`. Set `surface` to "frontend".
@@ -0,0 +1,10 @@
1
+ You turn two test runs into one short QA report for a ticket.
2
+
3
+ You are given the frontend run and the backend run. Write, in plain prose:
4
+
5
+ 1. Which test cases were carried out, naming each one.
6
+ 2. What the evidence was for each.
7
+ 3. Any findings, or a single sentence saying there were none.
8
+
9
+ Report only what the runs contain. Do not invent a case that nobody ran, and do not soften a
10
+ failure. Someone will check this report against the list of expected test cases.
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * A deterministic stand-in for Playwright MCP.
5
+ *
6
+ * The `mcp Browser` block in `agents.nornagent` is the *same shape* a real Playwright MCP
7
+ * declaration takes — see the commented block there — so this demo runs anywhere `node` does,
8
+ * with no browser download, while showing exactly what granting a browser looks like.
9
+ *
10
+ * It answers from a tiny scripted site, and its snapshots are text, which is what a browser
11
+ * agent should be working from anyway: image content never reaches a model's prompt.
12
+ */
13
+
14
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
15
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
16
+ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
17
+
18
+ const PAGES = {
19
+ '/login': {
20
+ title: 'Sign in',
21
+ elements: ['textbox "Email"', 'textbox "Password"', 'button "Sign in"']
22
+ },
23
+ '/orders': {
24
+ title: 'Your orders',
25
+ elements: ['link "A-1001"', 'link "A-1002"', 'button "New order"']
26
+ },
27
+ '/orders/A-1001': {
28
+ title: 'Order A-1001',
29
+ elements: ['heading "Order A-1001"', 'text "Total: 42.50"', 'button "Cancel order"']
30
+ }
31
+ };
32
+
33
+ const state = { url: '/login', signedIn: false, filled: {} };
34
+
35
+ const TOOLS = [
36
+ {
37
+ name: 'browser_navigate',
38
+ description: 'Navigate to a path on the site under test.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ additionalProperties: false,
42
+ properties: { path: { type: 'string', description: 'Path to open, e.g. /orders.' } },
43
+ required: ['path']
44
+ }
45
+ },
46
+ {
47
+ name: 'browser_fill',
48
+ description: 'Fill a named field on the current page.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ additionalProperties: false,
52
+ properties: {
53
+ field: { type: 'string', description: 'Field label, e.g. Email.' },
54
+ value: { type: 'string', description: 'Value to type.' }
55
+ },
56
+ required: ['field', 'value']
57
+ }
58
+ },
59
+ {
60
+ name: 'browser_click',
61
+ description: 'Click a named control on the current page.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ additionalProperties: false,
65
+ properties: { control: { type: 'string', description: 'Control label, e.g. Sign in.' } },
66
+ required: ['control']
67
+ }
68
+ },
69
+ {
70
+ name: 'browser_snapshot',
71
+ description: 'Return a text accessibility snapshot of the current page.',
72
+ inputSchema: { type: 'object', additionalProperties: false, properties: {} }
73
+ }
74
+ ];
75
+
76
+ function structured(value) {
77
+ return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value };
78
+ }
79
+
80
+ function snapshot() {
81
+ const page = PAGES[state.url] || { title: 'Not found', elements: [] };
82
+ return {
83
+ url: state.url,
84
+ title: page.title,
85
+ signedIn: state.signedIn,
86
+ elements: page.elements
87
+ };
88
+ }
89
+
90
+ async function main() {
91
+ const server = new Server(
92
+ { name: 'norn-demo-browser', version: '1.0.0' },
93
+ { capabilities: { tools: {} } }
94
+ );
95
+
96
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
97
+
98
+ server.setRequestHandler(CallToolRequestSchema, async request => {
99
+ const args = request.params.arguments || {};
100
+ switch (request.params.name) {
101
+ case 'browser_navigate':
102
+ state.url = String(args.path || '/');
103
+ if (state.url !== '/login' && !state.signedIn) {
104
+ // The whole point of a session: an unauthenticated visit lands on login.
105
+ state.url = '/login';
106
+ }
107
+ return structured(snapshot());
108
+ case 'browser_fill':
109
+ state.filled[String(args.field)] = String(args.value);
110
+ return structured({ filled: args.field, ...snapshot() });
111
+ case 'browser_click':
112
+ if (String(args.control) === 'Sign in' && state.filled.Email && state.filled.Password) {
113
+ state.signedIn = true;
114
+ state.url = '/orders';
115
+ }
116
+ return structured(snapshot());
117
+ case 'browser_snapshot':
118
+ return structured(snapshot());
119
+ default:
120
+ return {
121
+ isError: true,
122
+ content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }]
123
+ };
124
+ }
125
+ });
126
+
127
+ await server.connect(new StdioServerTransport());
128
+ }
129
+
130
+ main().catch(error => {
131
+ console.error(error instanceof Error ? error.message : String(error));
132
+ process.exit(1);
133
+ });
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * "House" — the custom MCP server a team writes for its own product.
5
+ *
6
+ * Deterministic on purpose: the demo is about declaring a server and granting it, not about
7
+ * what any particular tool does. Everything here is in-memory and reset per process.
8
+ */
9
+
10
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
11
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
12
+ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
13
+
14
+ const state = {
15
+ tenant: 'acme',
16
+ auditLog: [],
17
+ orders: [
18
+ { id: 'A-1001', tenant: 'acme', total: 42.5, status: 'placed' },
19
+ { id: 'A-1002', tenant: 'acme', total: 18.0, status: 'shipped' }
20
+ ]
21
+ };
22
+
23
+ const TOOLS = [
24
+ {
25
+ name: 'getFixtureUser',
26
+ description: 'Return the seeded test user for the current tenant.',
27
+ inputSchema: { type: 'object', additionalProperties: false, properties: {} }
28
+ },
29
+ {
30
+ name: 'resetTenant',
31
+ description: 'Reset the tenant to its seeded state before a test run.',
32
+ inputSchema: { type: 'object', additionalProperties: false, properties: {} }
33
+ },
34
+ {
35
+ name: 'callEndpoint',
36
+ description: 'Call a backend endpoint and return its status and body.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ additionalProperties: false,
40
+ properties: {
41
+ method: { type: 'string', description: 'HTTP method, e.g. GET or POST.' },
42
+ path: { type: 'string', description: 'Endpoint path, e.g. /orders/A-1001.' }
43
+ },
44
+ required: ['method', 'path']
45
+ }
46
+ },
47
+ {
48
+ name: 'readAuditLog',
49
+ description: 'Return the audit entries written during this run, oldest first.',
50
+ inputSchema: { type: 'object', additionalProperties: false, properties: {} }
51
+ },
52
+ {
53
+ name: 'queryDb',
54
+ description: 'Read rows from a seeded table by name.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ additionalProperties: false,
58
+ properties: { table: { type: 'string', description: 'Table to read, e.g. orders.' } },
59
+ required: ['table']
60
+ }
61
+ }
62
+ ];
63
+
64
+ function structured(value) {
65
+ return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value };
66
+ }
67
+
68
+ function callEndpoint(args) {
69
+ const path = String(args.path || '');
70
+ const method = String(args.method || 'GET').toUpperCase();
71
+ state.auditLog.push({ at: state.auditLog.length + 1, method, path });
72
+
73
+ const orderMatch = path.match(/^\/orders\/([A-Za-z0-9-]+)$/);
74
+ if (orderMatch) {
75
+ const order = state.orders.find(item => item.id === orderMatch[1]);
76
+ return order
77
+ ? { status: 200, body: order }
78
+ : { status: 404, body: { error: 'order not found' } };
79
+ }
80
+ if (path === '/orders') {
81
+ return { status: 200, body: { orders: state.orders } };
82
+ }
83
+ return { status: 404, body: { error: 'no such endpoint' } };
84
+ }
85
+
86
+ async function main() {
87
+ const server = new Server(
88
+ { name: 'norn-demo-house', version: '1.0.0' },
89
+ { capabilities: { tools: {} } }
90
+ );
91
+
92
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
93
+
94
+ server.setRequestHandler(CallToolRequestSchema, async request => {
95
+ const args = request.params.arguments || {};
96
+ switch (request.params.name) {
97
+ case 'getFixtureUser':
98
+ return structured({ id: 'u-1', email: 'tester@example.com', tenant: state.tenant });
99
+ case 'resetTenant':
100
+ state.auditLog = [];
101
+ return structured({ tenant: state.tenant, reset: true });
102
+ case 'callEndpoint':
103
+ return structured(callEndpoint(args));
104
+ case 'readAuditLog':
105
+ return structured({ entries: state.auditLog });
106
+ case 'queryDb':
107
+ return structured({
108
+ table: args.table,
109
+ rows: args.table === 'orders' ? state.orders : []
110
+ });
111
+ default:
112
+ return {
113
+ isError: true,
114
+ content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }]
115
+ };
116
+ }
117
+ });
118
+
119
+ await server.connect(new StdioServerTransport());
120
+ }
121
+
122
+ main().catch(error => {
123
+ console.error(error instanceof Error ? error.message : String(error));
124
+ process.exit(1);
125
+ });
@@ -0,0 +1,32 @@
1
+ import "./agents.nornagent"
2
+
3
+ # Prove both servers are reachable without spending anything on a model.
4
+ # These steps resolve the same aliases the agents use, so a failure here points at the server
5
+ # rather than at an agent — this is the loop to develop a server against.
6
+ test sequence ServersUp
7
+ var browserTools = run mcp list Browser
8
+ assert browserTools.length > 0
9
+
10
+ var houseTools = run mcp list House
11
+ assert houseTools.length > 0
12
+
13
+ var user = run mcp call House getFixtureUser()
14
+ assert user.structuredContent.email == "tester@example.com"
15
+ end sequence
16
+
17
+ # The application this whole slice was designed against: a ticket arrives, two agents test it
18
+ # from different sides with different tools, a third writes it up, and a judge rules on whether
19
+ # the expected test cases were actually covered.
20
+ test sequence TicketPROJ142
21
+ var ticket = run readJson "./fixtures/proj-142.json"
22
+
23
+ var frontend = run FrontendTester ticket
24
+ var backend = run BackendTester ticket
25
+ # An agent's input is a variable or a string — there is no object-literal form — so the two
26
+ # runs are interpolated into one prompt.
27
+ var report = run Reporter "Frontend run:\n{{frontend.body}}\n\nBackend run:\n{{backend.body}}"
28
+
29
+ print "QA report" | "{{report.text}}"
30
+
31
+ judge report with Reviewer expects file expectations/proj-142.md
32
+ end sequence