@trazum/mcp 1.8.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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/rpc.d.ts +79 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +205 -0
- package/dist/rpc.js.map +1 -0
- package/dist/tools.d.ts +32 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +213 -0
- package/dist/tools.js.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +28 -0
- package/src/rpc.ts +265 -0
- package/src/tools.ts +249 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Muñoz Rey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @trazum/mcp
|
|
2
|
+
|
|
3
|
+
Trazum as an [MCP](https://modelcontextprotocol.io) server, so an agent can price
|
|
4
|
+
and budget a prompt **before** it sends it.
|
|
5
|
+
|
|
6
|
+
Every other Trazum surface answers that question for a human after the fact — a CLI
|
|
7
|
+
you run, a page you paste into, a check that fails a build. This answers it for the
|
|
8
|
+
thing actually composing the prompt.
|
|
9
|
+
|
|
10
|
+
## It runs on your machine and costs nothing to host
|
|
11
|
+
|
|
12
|
+
One process over stdio, spawned by whatever client wants it, exactly like the CLI.
|
|
13
|
+
No service, nothing to keep up, and **no prompt leaves the machine**. Worth stating
|
|
14
|
+
because "MCP server" reads like infrastructure and this is not.
|
|
15
|
+
|
|
16
|
+
```jsonc
|
|
17
|
+
// Claude Code: .mcp.json — or the equivalent in any MCP client
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"trazum": { "command": "npx", "args": ["-y", "@trazum/mcp"] }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The tools
|
|
26
|
+
|
|
27
|
+
| Tool | Answers |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `check_prompt` | Does this prompt fit `maxTokens`? And if not, would optimising it fit? |
|
|
30
|
+
| `optimize_prompt` | The shorter text, the token counts either side, what the difference is worth per month, and any advisories. |
|
|
31
|
+
| `list_models` | Prices, context windows and cacheable minimums, with the date the table was reviewed. |
|
|
32
|
+
|
|
33
|
+
`check_prompt` is the one worth wiring up. It has **three** outcomes rather than
|
|
34
|
+
two, and the third is the point:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
OVER BUDGET — 2,140 tokens against 2,000, but the safe rules bring it to 1,870,
|
|
38
|
+
which fits. Optimise rather than cut.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
"Over budget" and "over budget but the rules would fix it" are different
|
|
42
|
+
instructions. A boolean throws away the actionable half.
|
|
43
|
+
|
|
44
|
+
## What it cannot do, which is the design
|
|
45
|
+
|
|
46
|
+
**No paths.** Every tool takes prompt text. A tool that accepted a filename would
|
|
47
|
+
be a file-read primitive reachable by whatever the model decided to ask for. This
|
|
48
|
+
package imports `@trazum/core`, the browser-safe entry point, and never
|
|
49
|
+
`@trazum/core/node` — the capability is *absent* rather than unused, and a test
|
|
50
|
+
enforces it.
|
|
51
|
+
|
|
52
|
+
**No network.** Nothing here calls a model. `--suggest` and `eval` exist in the CLI
|
|
53
|
+
and are deliberately not exposed: they spend your money, and a tool an agent can
|
|
54
|
+
invoke in a loop must not be able to do that.
|
|
55
|
+
|
|
56
|
+
**No writes.** The tools return figures. Applying them is the agent's job, in its
|
|
57
|
+
own context, where you can see the diff.
|
|
58
|
+
|
|
59
|
+
**Zero runtime dependencies outside this repository**, which is why the JSON-RPC
|
|
60
|
+
layer is written by hand rather than taken from the official SDK. That is not
|
|
61
|
+
preference. An MCP server reads prompts handed to it by a model, in a process you
|
|
62
|
+
did not start yourself, and every dependency is somebody else's code on that path.
|
|
63
|
+
The invariant applies here with more force than anywhere else in Trazum, so
|
|
64
|
+
relaxing it here would have been backwards.
|
|
65
|
+
|
|
66
|
+
## Limits, stated
|
|
67
|
+
|
|
68
|
+
The protocol implementation covers what a tools-only server needs — `initialize`,
|
|
69
|
+
`notifications/initialized`, `tools/list`, `tools/call`, `ping` — and answers
|
|
70
|
+
anything else with `-32601 Method not found`. No resources, no prompts, no
|
|
71
|
+
sampling. It is driven by a raw newline-delimited client in the tests; it has not
|
|
72
|
+
been driven by every MCP client in existence.
|
|
73
|
+
|
|
74
|
+
Token counts are estimates (±15% on prose, calibrated against Claude's tokenizer),
|
|
75
|
+
and every tool says so in its own output. A prompt within a few percent of its
|
|
76
|
+
budget should be treated as uncertain rather than as passing.
|
|
77
|
+
|
|
78
|
+
## Licence
|
|
79
|
+
|
|
80
|
+
MIT. Part of [Trazum](https://github.com/Davmunrey/Trazum).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { PROTOCOL_VERSION, serve } from './rpc.js';
|
|
3
|
+
import { TOOLS } from './tools.js';
|
|
4
|
+
/**
|
|
5
|
+
* Trazum as an MCP server.
|
|
6
|
+
*
|
|
7
|
+
* The point is narrow: an agent about to spend money on a prompt can ask what it
|
|
8
|
+
* will cost, and whether it busts a budget, *before* sending it. Every other
|
|
9
|
+
* surface here answers that for a human after the fact — a CLI you run, a page you
|
|
10
|
+
* paste into, a check that fails a build. This answers it for the thing actually
|
|
11
|
+
* composing the prompt.
|
|
12
|
+
*
|
|
13
|
+
* **It runs on the caller's machine and costs this project nothing.** One process
|
|
14
|
+
* over stdio, spawned by whatever client wants it, exactly like the CLI. There is
|
|
15
|
+
* no service to host, nothing to keep up, and no prompt leaves the machine. Worth
|
|
16
|
+
* saying because "MCP server" reads like infrastructure and this is not.
|
|
17
|
+
*/
|
|
18
|
+
serve(process.stdin, process.stdout, TOOLS, {
|
|
19
|
+
name: 'trazum',
|
|
20
|
+
version: '1.8.0',
|
|
21
|
+
instructions: 'Price and budget a prompt before sending it. Every tool takes prompt text and returns '
|
|
22
|
+
+ 'figures; none of them read files, reach the network or call a model. Token counts are '
|
|
23
|
+
+ `estimates, so treat a prompt within a few percent of its budget as uncertain. `
|
|
24
|
+
+ `Protocol ${PROTOCOL_VERSION}, tools only.`,
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;GAaG;AAEH,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE;IAC1C,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,OAAO;IAChB,YAAY,EACV,wFAAwF;UACtF,wFAAwF;UACxF,gFAAgF;UAChF,YAAY,gBAAgB,eAAe;CAChD,CAAC,CAAC"}
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 over stdio, by hand, because of what this package is.
|
|
3
|
+
*
|
|
4
|
+
* The obvious implementation imports `@modelcontextprotocol/sdk`. It was written
|
|
5
|
+
* that way first, and thirteen tests passed against a real process. Then
|
|
6
|
+
* `publish.test.js` refused it: *every* publishable package in this repository
|
|
7
|
+
* carries no runtime dependencies, and the reason `security.test.js` gives is
|
|
8
|
+
* "the core and the CLI process untrusted text; every runtime dependency is code
|
|
9
|
+
* that would run on that text with no review from this project."
|
|
10
|
+
*
|
|
11
|
+
* That argument applies here with **more** force than anywhere else in the
|
|
12
|
+
* repository, not less. An MCP server reads prompts handed to it by a model, in a
|
|
13
|
+
* process the user did not start by hand, and the SDK plus its own dependency
|
|
14
|
+
* tree is a large amount of somebody else's code sitting on that path. Relaxing
|
|
15
|
+
* the invariant at the exact point it matters most would have been the wrong
|
|
16
|
+
* trade, so the invariant won and this file exists.
|
|
17
|
+
*
|
|
18
|
+
* **What that costs, stated plainly.** A hand-written protocol is where subtle
|
|
19
|
+
* incompatibility lives. This implements the parts a tools-only server needs —
|
|
20
|
+
* `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, and
|
|
21
|
+
* `ping` — and nothing else. No resources, no prompts, no sampling, no
|
|
22
|
+
* completion, no server-initiated requests. A client asking for any of those gets
|
|
23
|
+
* a proper `-32601 Method not found` rather than silence. It has been driven by a
|
|
24
|
+
* raw newline-delimited client in the tests; it has not been driven by every real
|
|
25
|
+
* MCP client in existence, and that is the honest limit of the claim.
|
|
26
|
+
*/
|
|
27
|
+
/** The version this server implements. Echoed back when the client asks for it. */
|
|
28
|
+
export declare const PROTOCOL_VERSION = "2025-06-18";
|
|
29
|
+
export interface JsonRpcRequest {
|
|
30
|
+
jsonrpc: '2.0';
|
|
31
|
+
id?: string | number | null;
|
|
32
|
+
method: string;
|
|
33
|
+
params?: unknown;
|
|
34
|
+
}
|
|
35
|
+
/** Standard JSON-RPC codes, plus the one MCP adds for bad tool arguments. */
|
|
36
|
+
export declare const ERROR: {
|
|
37
|
+
readonly parse: -32700;
|
|
38
|
+
readonly invalidRequest: -32600;
|
|
39
|
+
readonly methodNotFound: -32601;
|
|
40
|
+
readonly invalidParams: -32602;
|
|
41
|
+
readonly internal: -32603;
|
|
42
|
+
};
|
|
43
|
+
export interface ToolDefinition {
|
|
44
|
+
name: string;
|
|
45
|
+
title: string;
|
|
46
|
+
description: string;
|
|
47
|
+
/** JSON Schema, written out, since there is no schema library to build one. */
|
|
48
|
+
inputSchema: Record<string, unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* Validates and coerces the arguments, or throws with a message the model can
|
|
51
|
+
* act on. Returns the text to send back.
|
|
52
|
+
*/
|
|
53
|
+
run: (args: Record<string, unknown>) => string;
|
|
54
|
+
}
|
|
55
|
+
/** Thrown by a tool when its arguments are wrong. Distinguished from a crash. */
|
|
56
|
+
export declare class InvalidArguments extends Error {
|
|
57
|
+
}
|
|
58
|
+
export interface ServerInfo {
|
|
59
|
+
name: string;
|
|
60
|
+
version: string;
|
|
61
|
+
instructions: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Handles one decoded message and returns the response, or `null` for a
|
|
65
|
+
* notification.
|
|
66
|
+
*
|
|
67
|
+
* Pure: takes a message, returns a message. The transport below is what touches
|
|
68
|
+
* streams, so every dispatch rule here is testable without a process.
|
|
69
|
+
*/
|
|
70
|
+
export declare function handle(message: unknown, tools: readonly ToolDefinition[], info: ServerInfo): object | null;
|
|
71
|
+
/**
|
|
72
|
+
* Reads newline-delimited JSON from a stream and writes replies to another.
|
|
73
|
+
*
|
|
74
|
+
* Line-delimited rather than the Content-Length framing LSP uses, because that is
|
|
75
|
+
* what MCP's stdio transport specifies. A message may not contain a raw newline,
|
|
76
|
+
* which `JSON.stringify` guarantees.
|
|
77
|
+
*/
|
|
78
|
+
export declare function serve(input: NodeJS.ReadableStream, output: NodeJS.WritableStream, tools: readonly ToolDefinition[], info: ServerInfo): void;
|
|
79
|
+
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,6EAA6E;AAC7E,eAAO,MAAM,KAAK;aAChB,KAAK,EAAE,CAAC,KAAK;aACb,cAAc,EAAE,CAAC,KAAK;aACtB,cAAc,EAAE,CAAC,KAAK;aACtB,aAAa,EAAE,CAAC,KAAK;aACrB,QAAQ,EAAE,CAAC,KAAK;CACR,CAAC;AAEX,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC;;;OAGG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC;CAChD;AAED,iFAAiF;AACjF,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,IAAI,EAAE,UAAU,GACf,MAAM,GAAG,IAAI,CAuHf;AAUD;;;;;;GAMG;AACH,wBAAgB,KAAK,CACnB,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,MAAM,EAAE,MAAM,CAAC,cAAc,EAC7B,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,IAAI,EAAE,UAAU,GACf,IAAI,CAwCN"}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 over stdio, by hand, because of what this package is.
|
|
3
|
+
*
|
|
4
|
+
* The obvious implementation imports `@modelcontextprotocol/sdk`. It was written
|
|
5
|
+
* that way first, and thirteen tests passed against a real process. Then
|
|
6
|
+
* `publish.test.js` refused it: *every* publishable package in this repository
|
|
7
|
+
* carries no runtime dependencies, and the reason `security.test.js` gives is
|
|
8
|
+
* "the core and the CLI process untrusted text; every runtime dependency is code
|
|
9
|
+
* that would run on that text with no review from this project."
|
|
10
|
+
*
|
|
11
|
+
* That argument applies here with **more** force than anywhere else in the
|
|
12
|
+
* repository, not less. An MCP server reads prompts handed to it by a model, in a
|
|
13
|
+
* process the user did not start by hand, and the SDK plus its own dependency
|
|
14
|
+
* tree is a large amount of somebody else's code sitting on that path. Relaxing
|
|
15
|
+
* the invariant at the exact point it matters most would have been the wrong
|
|
16
|
+
* trade, so the invariant won and this file exists.
|
|
17
|
+
*
|
|
18
|
+
* **What that costs, stated plainly.** A hand-written protocol is where subtle
|
|
19
|
+
* incompatibility lives. This implements the parts a tools-only server needs —
|
|
20
|
+
* `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, and
|
|
21
|
+
* `ping` — and nothing else. No resources, no prompts, no sampling, no
|
|
22
|
+
* completion, no server-initiated requests. A client asking for any of those gets
|
|
23
|
+
* a proper `-32601 Method not found` rather than silence. It has been driven by a
|
|
24
|
+
* raw newline-delimited client in the tests; it has not been driven by every real
|
|
25
|
+
* MCP client in existence, and that is the honest limit of the claim.
|
|
26
|
+
*/
|
|
27
|
+
/** The version this server implements. Echoed back when the client asks for it. */
|
|
28
|
+
export const PROTOCOL_VERSION = '2025-06-18';
|
|
29
|
+
/** Standard JSON-RPC codes, plus the one MCP adds for bad tool arguments. */
|
|
30
|
+
export const ERROR = {
|
|
31
|
+
parse: -32700,
|
|
32
|
+
invalidRequest: -32600,
|
|
33
|
+
methodNotFound: -32601,
|
|
34
|
+
invalidParams: -32602,
|
|
35
|
+
internal: -32603,
|
|
36
|
+
};
|
|
37
|
+
/** Thrown by a tool when its arguments are wrong. Distinguished from a crash. */
|
|
38
|
+
export class InvalidArguments extends Error {
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Handles one decoded message and returns the response, or `null` for a
|
|
42
|
+
* notification.
|
|
43
|
+
*
|
|
44
|
+
* Pure: takes a message, returns a message. The transport below is what touches
|
|
45
|
+
* streams, so every dispatch rule here is testable without a process.
|
|
46
|
+
*/
|
|
47
|
+
export function handle(message, tools, info) {
|
|
48
|
+
if (typeof message !== 'object' || message === null) {
|
|
49
|
+
return errorFor(null, ERROR.invalidRequest, 'a message must be an object');
|
|
50
|
+
}
|
|
51
|
+
const request = message;
|
|
52
|
+
const id = request.id ?? null;
|
|
53
|
+
const isNotification = request.id === undefined;
|
|
54
|
+
if (request.jsonrpc !== '2.0') {
|
|
55
|
+
return isNotification ? null : errorFor(id, ERROR.invalidRequest, 'jsonrpc must be "2.0"');
|
|
56
|
+
}
|
|
57
|
+
if (typeof request.method !== 'string') {
|
|
58
|
+
return isNotification ? null : errorFor(id, ERROR.invalidRequest, 'method must be a string');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Notifications get no reply, ever — and this check belongs *before* the switch.
|
|
62
|
+
*
|
|
63
|
+
* It was inside it at first, listing the two `notifications/*` methods by name,
|
|
64
|
+
* which is the wrong rule: a notification is defined by the **absence of an
|
|
65
|
+
* id**, not by its method. `{"jsonrpc":"2.0","method":"initialize"}` with no id
|
|
66
|
+
* is a notification, and that version answered it. Replying to a notification
|
|
67
|
+
* is a protocol violation some clients tolerate and others hang on, which is
|
|
68
|
+
* the worst kind to have because it works in testing. Found by a test that
|
|
69
|
+
* asked for the rule rather than for the two names.
|
|
70
|
+
*/
|
|
71
|
+
if (isNotification)
|
|
72
|
+
return null;
|
|
73
|
+
switch (request.method) {
|
|
74
|
+
case 'initialize': {
|
|
75
|
+
const params = (request.params ?? {});
|
|
76
|
+
/**
|
|
77
|
+
* Echo the client's version when it is a string, otherwise offer ours.
|
|
78
|
+
*
|
|
79
|
+
* A client that speaks a version this server has never heard of is still
|
|
80
|
+
* better served by being told what it asked for than by a hard refusal:
|
|
81
|
+
* every method below is version-independent, and the alternative is
|
|
82
|
+
* refusing to start over a string.
|
|
83
|
+
*/
|
|
84
|
+
const version = typeof params.protocolVersion === 'string' ? params.protocolVersion : PROTOCOL_VERSION;
|
|
85
|
+
return {
|
|
86
|
+
jsonrpc: '2.0',
|
|
87
|
+
id,
|
|
88
|
+
result: {
|
|
89
|
+
protocolVersion: version,
|
|
90
|
+
// Tools only. Declaring capabilities this server does not implement is
|
|
91
|
+
// how a client comes to ask for one and get an error it did not expect.
|
|
92
|
+
capabilities: { tools: {} },
|
|
93
|
+
serverInfo: { name: info.name, version: info.version },
|
|
94
|
+
instructions: info.instructions,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case 'ping':
|
|
99
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
100
|
+
case 'tools/list':
|
|
101
|
+
return {
|
|
102
|
+
jsonrpc: '2.0',
|
|
103
|
+
id,
|
|
104
|
+
result: {
|
|
105
|
+
tools: tools.map((tool) => ({
|
|
106
|
+
name: tool.name,
|
|
107
|
+
title: tool.title,
|
|
108
|
+
description: tool.description,
|
|
109
|
+
inputSchema: tool.inputSchema,
|
|
110
|
+
})),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
case 'tools/call': {
|
|
114
|
+
const params = (request.params ?? {});
|
|
115
|
+
if (typeof params.name !== 'string') {
|
|
116
|
+
return errorFor(id, ERROR.invalidParams, 'name must be a string');
|
|
117
|
+
}
|
|
118
|
+
const tool = tools.find((candidate) => candidate.name === params.name);
|
|
119
|
+
if (!tool) {
|
|
120
|
+
return errorFor(id, ERROR.invalidParams, `unknown tool: ${params.name}`);
|
|
121
|
+
}
|
|
122
|
+
const args = typeof params.arguments === 'object' && params.arguments !== null
|
|
123
|
+
? params.arguments
|
|
124
|
+
: {};
|
|
125
|
+
try {
|
|
126
|
+
return {
|
|
127
|
+
jsonrpc: '2.0',
|
|
128
|
+
id,
|
|
129
|
+
result: { content: [{ type: 'text', text: tool.run(args) }] },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
/**
|
|
134
|
+
* A tool failure is a *result* with `isError`, not a JSON-RPC error.
|
|
135
|
+
*
|
|
136
|
+
* The distinction is the whole point of that flag: a protocol error means
|
|
137
|
+
* the client is broken, while `isError` means the model asked for
|
|
138
|
+
* something it should ask differently, and the model is the one that
|
|
139
|
+
* needs to read the message. Sending a protocol error here would hide the
|
|
140
|
+
* explanation from the only party able to act on it.
|
|
141
|
+
*/
|
|
142
|
+
const text = error instanceof InvalidArguments
|
|
143
|
+
? error.message
|
|
144
|
+
: `the tool failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
145
|
+
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
return errorFor(id, ERROR.methodNotFound, `this server implements tools only: ${request.method}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function errorFor(id, code, message) {
|
|
153
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reads newline-delimited JSON from a stream and writes replies to another.
|
|
157
|
+
*
|
|
158
|
+
* Line-delimited rather than the Content-Length framing LSP uses, because that is
|
|
159
|
+
* what MCP's stdio transport specifies. A message may not contain a raw newline,
|
|
160
|
+
* which `JSON.stringify` guarantees.
|
|
161
|
+
*/
|
|
162
|
+
export function serve(input, output, tools, info) {
|
|
163
|
+
let buffer = '';
|
|
164
|
+
/**
|
|
165
|
+
* A cap, because the peer is not necessarily well behaved.
|
|
166
|
+
*
|
|
167
|
+
* Without one, a stream that never sends a newline grows this string until the
|
|
168
|
+
* process dies of memory exhaustion — a denial of service that needs no
|
|
169
|
+
* malice, just a client with a bug.
|
|
170
|
+
*/
|
|
171
|
+
const MAX_LINE = 8 * 1024 * 1024;
|
|
172
|
+
input.setEncoding('utf8');
|
|
173
|
+
input.on('data', (chunk) => {
|
|
174
|
+
buffer += chunk;
|
|
175
|
+
if (buffer.length > MAX_LINE) {
|
|
176
|
+
buffer = '';
|
|
177
|
+
write(output, errorFor(null, ERROR.invalidRequest, 'message exceeded 8 MiB'));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let newline = buffer.indexOf('\n');
|
|
181
|
+
while (newline !== -1) {
|
|
182
|
+
const line = buffer.slice(0, newline).trim();
|
|
183
|
+
buffer = buffer.slice(newline + 1);
|
|
184
|
+
if (line !== '') {
|
|
185
|
+
let decoded;
|
|
186
|
+
try {
|
|
187
|
+
decoded = JSON.parse(line);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
write(output, errorFor(null, ERROR.parse, 'not valid JSON'));
|
|
191
|
+
newline = buffer.indexOf('\n');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const response = handle(decoded, tools, info);
|
|
195
|
+
if (response !== null)
|
|
196
|
+
write(output, response);
|
|
197
|
+
}
|
|
198
|
+
newline = buffer.indexOf('\n');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function write(output, message) {
|
|
203
|
+
output.write(`${JSON.stringify(message)}\n`);
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=rpc.js.map
|
package/dist/rpc.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ToolDefinition } from './rpc.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tools, kept in one file so the whole surface an agent can reach reads in one
|
|
4
|
+
* pass.
|
|
5
|
+
*
|
|
6
|
+
* **Three deliberate absences, and they are the security design.**
|
|
7
|
+
*
|
|
8
|
+
* *No paths.* Every tool takes prompt text. A tool that accepted a filename would
|
|
9
|
+
* be a file-read primitive reachable by whatever the model decided to ask for, and
|
|
10
|
+
* "we reviewed it" is not a durable defence against one being added later. This
|
|
11
|
+
* package imports `@trazum/core`, the browser-safe entry point, and never
|
|
12
|
+
* `@trazum/core/node` — so the capability is *absent* rather than unused, and a
|
|
13
|
+
* test enforces that.
|
|
14
|
+
*
|
|
15
|
+
* *No network.* Nothing here calls a model. `--suggest` and `eval` exist in the
|
|
16
|
+
* CLI and are deliberately not exposed: they spend the caller's money, and a tool
|
|
17
|
+
* an agent can invoke in a loop must not be able to do that. Everything below is
|
|
18
|
+
* arithmetic on text.
|
|
19
|
+
*
|
|
20
|
+
* *No writes.* The tools return figures. Applying them is the agent's job, in its
|
|
21
|
+
* own context, where a human can see the diff.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The same cap the web API uses, for the same reason.
|
|
25
|
+
*
|
|
26
|
+
* An agent in a loop is exactly the caller that hands you a 40 MB string by
|
|
27
|
+
* accident. Refusing early with a number beats an unbounded pass over it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MAX_PROMPT_CHARS = 400000;
|
|
30
|
+
/** The whole surface. An exact list, asserted as one by the tests. */
|
|
31
|
+
export declare const TOOLS: readonly ToolDefinition[];
|
|
32
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,SAAU,CAAC;AA+MxC,sEAAsE;AACtE,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EAA8B,CAAC"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { BUNDLED_CATALOGUE, PRICING_LAST_REVIEWED, formatUsd, listModels, optimize, } from '@trazum/core';
|
|
2
|
+
import { InvalidArguments } from './rpc.js';
|
|
3
|
+
/**
|
|
4
|
+
* The tools, kept in one file so the whole surface an agent can reach reads in one
|
|
5
|
+
* pass.
|
|
6
|
+
*
|
|
7
|
+
* **Three deliberate absences, and they are the security design.**
|
|
8
|
+
*
|
|
9
|
+
* *No paths.* Every tool takes prompt text. A tool that accepted a filename would
|
|
10
|
+
* be a file-read primitive reachable by whatever the model decided to ask for, and
|
|
11
|
+
* "we reviewed it" is not a durable defence against one being added later. This
|
|
12
|
+
* package imports `@trazum/core`, the browser-safe entry point, and never
|
|
13
|
+
* `@trazum/core/node` — so the capability is *absent* rather than unused, and a
|
|
14
|
+
* test enforces that.
|
|
15
|
+
*
|
|
16
|
+
* *No network.* Nothing here calls a model. `--suggest` and `eval` exist in the
|
|
17
|
+
* CLI and are deliberately not exposed: they spend the caller's money, and a tool
|
|
18
|
+
* an agent can invoke in a loop must not be able to do that. Everything below is
|
|
19
|
+
* arithmetic on text.
|
|
20
|
+
*
|
|
21
|
+
* *No writes.* The tools return figures. Applying them is the agent's job, in its
|
|
22
|
+
* own context, where a human can see the diff.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The same cap the web API uses, for the same reason.
|
|
26
|
+
*
|
|
27
|
+
* An agent in a loop is exactly the caller that hands you a 40 MB string by
|
|
28
|
+
* accident. Refusing early with a number beats an unbounded pass over it.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_PROMPT_CHARS = 400_000;
|
|
31
|
+
/** Every figure this server prints descends from the estimator, so it says so. */
|
|
32
|
+
const BAND_NOTE = 'token counts are estimates (±15% on prose, calibrated on Claude); prices reviewed '
|
|
33
|
+
+ PRICING_LAST_REVIEWED;
|
|
34
|
+
function promptFrom(args) {
|
|
35
|
+
const prompt = args.prompt;
|
|
36
|
+
if (typeof prompt !== 'string')
|
|
37
|
+
throw new InvalidArguments('prompt must be a string');
|
|
38
|
+
if (prompt.length === 0)
|
|
39
|
+
throw new InvalidArguments('prompt is empty');
|
|
40
|
+
if (prompt.length > MAX_PROMPT_CHARS) {
|
|
41
|
+
throw new InvalidArguments(`prompt is ${prompt.length} characters, over the ${MAX_PROMPT_CHARS} limit`);
|
|
42
|
+
}
|
|
43
|
+
return prompt;
|
|
44
|
+
}
|
|
45
|
+
function levelFrom(args) {
|
|
46
|
+
const level = args.level ?? 'safe';
|
|
47
|
+
if (level !== 'safe' && level !== 'aggressive') {
|
|
48
|
+
throw new InvalidArguments('level must be "safe" or "aggressive"');
|
|
49
|
+
}
|
|
50
|
+
return level;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A positive integer, or the default.
|
|
54
|
+
*
|
|
55
|
+
* Written out rather than reached for from a validation library, and bounded on
|
|
56
|
+
* both ends: `callsPerMonth: 1e308` would otherwise produce an Infinity in
|
|
57
|
+
* somebody's budget, which is worse than a refusal because it looks like an
|
|
58
|
+
* answer.
|
|
59
|
+
*/
|
|
60
|
+
function intFrom(args, key, fallback, { min, max }) {
|
|
61
|
+
const raw = args[key];
|
|
62
|
+
if (raw === undefined)
|
|
63
|
+
return fallback;
|
|
64
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw)) {
|
|
65
|
+
throw new InvalidArguments(`${key} must be an integer`);
|
|
66
|
+
}
|
|
67
|
+
if (raw < min || raw > max) {
|
|
68
|
+
throw new InvalidArguments(`${key} must be between ${min} and ${max}`);
|
|
69
|
+
}
|
|
70
|
+
return raw;
|
|
71
|
+
}
|
|
72
|
+
const PROMPT_PROPERTY = {
|
|
73
|
+
type: 'string',
|
|
74
|
+
minLength: 1,
|
|
75
|
+
maxLength: MAX_PROMPT_CHARS,
|
|
76
|
+
description: 'The prompt text itself. This server never reads files.',
|
|
77
|
+
};
|
|
78
|
+
const LEVEL_PROPERTY = {
|
|
79
|
+
type: 'string',
|
|
80
|
+
enum: ['safe', 'aggressive'],
|
|
81
|
+
default: 'safe',
|
|
82
|
+
description: 'safe leaves meaning untouched; aggressive also rewords, and wants reading',
|
|
83
|
+
};
|
|
84
|
+
const OPTIMIZE = {
|
|
85
|
+
name: 'optimize_prompt',
|
|
86
|
+
title: 'Optimise a prompt and price the difference',
|
|
87
|
+
description: "Applies Trazum's deterministic rules to a prompt and returns the shorter text, the "
|
|
88
|
+
+ 'token counts either side, what the difference is worth per month, and any advisories. '
|
|
89
|
+
+ 'Offline and free: no model is called.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
prompt: PROMPT_PROPERTY,
|
|
94
|
+
level: LEVEL_PROPERTY,
|
|
95
|
+
model: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
default: 'claude-opus-5',
|
|
98
|
+
description: 'Model id used for pricing. Call list_models for what is known.',
|
|
99
|
+
},
|
|
100
|
+
callsPerMonth: {
|
|
101
|
+
type: 'integer',
|
|
102
|
+
minimum: 1,
|
|
103
|
+
maximum: 1_000_000_000,
|
|
104
|
+
default: 1000,
|
|
105
|
+
description: 'Used only to scale the figures',
|
|
106
|
+
},
|
|
107
|
+
avgOutputTokens: { type: 'integer', minimum: 0, maximum: 1_000_000, default: 500 },
|
|
108
|
+
},
|
|
109
|
+
required: ['prompt'],
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
},
|
|
112
|
+
run: (args) => {
|
|
113
|
+
const model = args.model ?? 'claude-opus-5';
|
|
114
|
+
if (typeof model !== 'string')
|
|
115
|
+
throw new InvalidArguments('model must be a string');
|
|
116
|
+
const callsPerMonth = intFrom(args, 'callsPerMonth', 1000, { min: 1, max: 1_000_000_000 });
|
|
117
|
+
const avgOutputTokens = intFrom(args, 'avgOutputTokens', 500, { min: 0, max: 1_000_000 });
|
|
118
|
+
const result = optimize(promptFrom(args), {
|
|
119
|
+
level: levelFrom(args),
|
|
120
|
+
usage: { model, callsPerMonth, avgOutputTokens },
|
|
121
|
+
});
|
|
122
|
+
const lines = [
|
|
123
|
+
`tokens: ${result.tokensBefore} → ${result.tokensAfter}`
|
|
124
|
+
+ ` (${result.tokensBefore - result.tokensAfter} fewer)`,
|
|
125
|
+
`monthly saving at ${callsPerMonth.toLocaleString('en-US')} calls:`
|
|
126
|
+
+ ` ${formatUsd(result.savings.monthlySavingsUsd)}`,
|
|
127
|
+
BAND_NOTE,
|
|
128
|
+
'',
|
|
129
|
+
'--- optimised prompt ---',
|
|
130
|
+
result.optimized,
|
|
131
|
+
];
|
|
132
|
+
if (result.advisories.length > 0) {
|
|
133
|
+
lines.push('', '--- advisories ---');
|
|
134
|
+
for (const advisory of result.advisories) {
|
|
135
|
+
const money = advisory.estimatedMonthlyUsd === null
|
|
136
|
+
? ''
|
|
137
|
+
: ` (~${formatUsd(advisory.estimatedMonthlyUsd)}/month)`;
|
|
138
|
+
lines.push(`[${advisory.id}] ${advisory.title}${money}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const CHECK = {
|
|
145
|
+
name: 'check_prompt',
|
|
146
|
+
title: 'Check a prompt against a token budget',
|
|
147
|
+
description: 'Answers whether a prompt fits a maximum, and if not, whether optimising it would. '
|
|
148
|
+
+ 'This is the one to call before sending a prompt you are unsure about.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
prompt: PROMPT_PROPERTY,
|
|
153
|
+
maxTokens: {
|
|
154
|
+
type: 'integer',
|
|
155
|
+
minimum: 1,
|
|
156
|
+
description: 'The budget. Required: a check with no maximum is not a check.',
|
|
157
|
+
},
|
|
158
|
+
level: LEVEL_PROPERTY,
|
|
159
|
+
},
|
|
160
|
+
required: ['prompt', 'maxTokens'],
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
},
|
|
163
|
+
run: (args) => {
|
|
164
|
+
if (args.maxTokens === undefined)
|
|
165
|
+
throw new InvalidArguments('maxTokens is required');
|
|
166
|
+
const maxTokens = intFrom(args, 'maxTokens', 0, { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
167
|
+
const level = levelFrom(args);
|
|
168
|
+
const result = optimize(promptFrom(args), { level });
|
|
169
|
+
/**
|
|
170
|
+
* Three outcomes, not two, and the third is why this tool exists.
|
|
171
|
+
*
|
|
172
|
+
* "Over budget" and "over budget but the rules would fix it" are different
|
|
173
|
+
* instructions to whoever asked: one means cut content, the other means run
|
|
174
|
+
* the rules. A boolean throws away the actionable half.
|
|
175
|
+
*/
|
|
176
|
+
const verdict = result.tokensBefore <= maxTokens
|
|
177
|
+
? `PASS — ${result.tokensBefore} tokens, budget ${maxTokens}`
|
|
178
|
+
: result.tokensAfter <= maxTokens
|
|
179
|
+
? `OVER BUDGET — ${result.tokensBefore} tokens against ${maxTokens}, but the ${level}`
|
|
180
|
+
+ ` rules bring it to ${result.tokensAfter}, which fits. Optimise rather than cut.`
|
|
181
|
+
: `OVER BUDGET — ${result.tokensBefore} tokens against ${maxTokens}. Even optimised it`
|
|
182
|
+
+ ` is ${result.tokensAfter}: content has to be cut.`;
|
|
183
|
+
return [
|
|
184
|
+
verdict,
|
|
185
|
+
'token counts are estimates (±15% on prose, calibrated on Claude), so a prompt within'
|
|
186
|
+
+ ' a few percent of its budget should be treated as uncertain',
|
|
187
|
+
].join('\n');
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
const MODELS = {
|
|
191
|
+
name: 'list_models',
|
|
192
|
+
title: 'Models Trazum can price, and their rates',
|
|
193
|
+
description: 'Input and output price per million tokens, context window and cacheable minimum, for '
|
|
194
|
+
+ 'every model in the bundled catalogue.',
|
|
195
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
196
|
+
run: () => {
|
|
197
|
+
const rows = listModels().map((model) => {
|
|
198
|
+
const cache = model.caching === 'none' || model.cacheMinTokens === null
|
|
199
|
+
? 'no caching'
|
|
200
|
+
: `cache min ${model.cacheMinTokens}`;
|
|
201
|
+
return `${model.id} in $${model.inputPerMTok}/Mtok out $${model.outputPerMTok}/Mtok`
|
|
202
|
+
+ ` context ${model.contextWindow.toLocaleString('en-US')} ${cache}`;
|
|
203
|
+
});
|
|
204
|
+
return [
|
|
205
|
+
`prices reviewed ${BUNDLED_CATALOGUE.lastReviewed} — verify before budgeting`,
|
|
206
|
+
'',
|
|
207
|
+
...rows,
|
|
208
|
+
].join('\n');
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
/** The whole surface. An exact list, asserted as one by the tests. */
|
|
212
|
+
export const TOOLS = [OPTIMIZE, CHECK, MODELS];
|
|
213
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trazum/mcp",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Trazum as an MCP server: let an agent price and budget its own prompts before it sends them.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "David Mu\u00f1oz Rey",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Davmunrey/Trazum.git",
|
|
10
|
+
"directory": "packages/mcp"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"model-context-protocol",
|
|
15
|
+
"prompt",
|
|
16
|
+
"llm",
|
|
17
|
+
"tokens",
|
|
18
|
+
"cost"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"trazum-mcp": "dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
37
|
+
"prepublishOnly": "npm run build && npm test"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@trazum/core": "1.8.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^26.2.0",
|
|
44
|
+
"typescript": "^7.0.2"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { PROTOCOL_VERSION, serve } from './rpc.js';
|
|
3
|
+
import { TOOLS } from './tools.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trazum as an MCP server.
|
|
7
|
+
*
|
|
8
|
+
* The point is narrow: an agent about to spend money on a prompt can ask what it
|
|
9
|
+
* will cost, and whether it busts a budget, *before* sending it. Every other
|
|
10
|
+
* surface here answers that for a human after the fact — a CLI you run, a page you
|
|
11
|
+
* paste into, a check that fails a build. This answers it for the thing actually
|
|
12
|
+
* composing the prompt.
|
|
13
|
+
*
|
|
14
|
+
* **It runs on the caller's machine and costs this project nothing.** One process
|
|
15
|
+
* over stdio, spawned by whatever client wants it, exactly like the CLI. There is
|
|
16
|
+
* no service to host, nothing to keep up, and no prompt leaves the machine. Worth
|
|
17
|
+
* saying because "MCP server" reads like infrastructure and this is not.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
serve(process.stdin, process.stdout, TOOLS, {
|
|
21
|
+
name: 'trazum',
|
|
22
|
+
version: '1.8.0',
|
|
23
|
+
instructions:
|
|
24
|
+
'Price and budget a prompt before sending it. Every tool takes prompt text and returns '
|
|
25
|
+
+ 'figures; none of them read files, reach the network or call a model. Token counts are '
|
|
26
|
+
+ `estimates, so treat a prompt within a few percent of its budget as uncertain. `
|
|
27
|
+
+ `Protocol ${PROTOCOL_VERSION}, tools only.`,
|
|
28
|
+
});
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 over stdio, by hand, because of what this package is.
|
|
3
|
+
*
|
|
4
|
+
* The obvious implementation imports `@modelcontextprotocol/sdk`. It was written
|
|
5
|
+
* that way first, and thirteen tests passed against a real process. Then
|
|
6
|
+
* `publish.test.js` refused it: *every* publishable package in this repository
|
|
7
|
+
* carries no runtime dependencies, and the reason `security.test.js` gives is
|
|
8
|
+
* "the core and the CLI process untrusted text; every runtime dependency is code
|
|
9
|
+
* that would run on that text with no review from this project."
|
|
10
|
+
*
|
|
11
|
+
* That argument applies here with **more** force than anywhere else in the
|
|
12
|
+
* repository, not less. An MCP server reads prompts handed to it by a model, in a
|
|
13
|
+
* process the user did not start by hand, and the SDK plus its own dependency
|
|
14
|
+
* tree is a large amount of somebody else's code sitting on that path. Relaxing
|
|
15
|
+
* the invariant at the exact point it matters most would have been the wrong
|
|
16
|
+
* trade, so the invariant won and this file exists.
|
|
17
|
+
*
|
|
18
|
+
* **What that costs, stated plainly.** A hand-written protocol is where subtle
|
|
19
|
+
* incompatibility lives. This implements the parts a tools-only server needs —
|
|
20
|
+
* `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, and
|
|
21
|
+
* `ping` — and nothing else. No resources, no prompts, no sampling, no
|
|
22
|
+
* completion, no server-initiated requests. A client asking for any of those gets
|
|
23
|
+
* a proper `-32601 Method not found` rather than silence. It has been driven by a
|
|
24
|
+
* raw newline-delimited client in the tests; it has not been driven by every real
|
|
25
|
+
* MCP client in existence, and that is the honest limit of the claim.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The version this server implements. Echoed back when the client asks for it. */
|
|
29
|
+
export const PROTOCOL_VERSION = '2025-06-18';
|
|
30
|
+
|
|
31
|
+
export interface JsonRpcRequest {
|
|
32
|
+
jsonrpc: '2.0';
|
|
33
|
+
id?: string | number | null;
|
|
34
|
+
method: string;
|
|
35
|
+
params?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Standard JSON-RPC codes, plus the one MCP adds for bad tool arguments. */
|
|
39
|
+
export const ERROR = {
|
|
40
|
+
parse: -32700,
|
|
41
|
+
invalidRequest: -32600,
|
|
42
|
+
methodNotFound: -32601,
|
|
43
|
+
invalidParams: -32602,
|
|
44
|
+
internal: -32603,
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export interface ToolDefinition {
|
|
48
|
+
name: string;
|
|
49
|
+
title: string;
|
|
50
|
+
description: string;
|
|
51
|
+
/** JSON Schema, written out, since there is no schema library to build one. */
|
|
52
|
+
inputSchema: Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Validates and coerces the arguments, or throws with a message the model can
|
|
55
|
+
* act on. Returns the text to send back.
|
|
56
|
+
*/
|
|
57
|
+
run: (args: Record<string, unknown>) => string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Thrown by a tool when its arguments are wrong. Distinguished from a crash. */
|
|
61
|
+
export class InvalidArguments extends Error {}
|
|
62
|
+
|
|
63
|
+
export interface ServerInfo {
|
|
64
|
+
name: string;
|
|
65
|
+
version: string;
|
|
66
|
+
instructions: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handles one decoded message and returns the response, or `null` for a
|
|
71
|
+
* notification.
|
|
72
|
+
*
|
|
73
|
+
* Pure: takes a message, returns a message. The transport below is what touches
|
|
74
|
+
* streams, so every dispatch rule here is testable without a process.
|
|
75
|
+
*/
|
|
76
|
+
export function handle(
|
|
77
|
+
message: unknown,
|
|
78
|
+
tools: readonly ToolDefinition[],
|
|
79
|
+
info: ServerInfo,
|
|
80
|
+
): object | null {
|
|
81
|
+
if (typeof message !== 'object' || message === null) {
|
|
82
|
+
return errorFor(null, ERROR.invalidRequest, 'a message must be an object');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const request = message as JsonRpcRequest;
|
|
86
|
+
const id = request.id ?? null;
|
|
87
|
+
const isNotification = request.id === undefined;
|
|
88
|
+
|
|
89
|
+
if (request.jsonrpc !== '2.0') {
|
|
90
|
+
return isNotification ? null : errorFor(id, ERROR.invalidRequest, 'jsonrpc must be "2.0"');
|
|
91
|
+
}
|
|
92
|
+
if (typeof request.method !== 'string') {
|
|
93
|
+
return isNotification ? null : errorFor(id, ERROR.invalidRequest, 'method must be a string');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Notifications get no reply, ever — and this check belongs *before* the switch.
|
|
98
|
+
*
|
|
99
|
+
* It was inside it at first, listing the two `notifications/*` methods by name,
|
|
100
|
+
* which is the wrong rule: a notification is defined by the **absence of an
|
|
101
|
+
* id**, not by its method. `{"jsonrpc":"2.0","method":"initialize"}` with no id
|
|
102
|
+
* is a notification, and that version answered it. Replying to a notification
|
|
103
|
+
* is a protocol violation some clients tolerate and others hang on, which is
|
|
104
|
+
* the worst kind to have because it works in testing. Found by a test that
|
|
105
|
+
* asked for the rule rather than for the two names.
|
|
106
|
+
*/
|
|
107
|
+
if (isNotification) return null;
|
|
108
|
+
|
|
109
|
+
switch (request.method) {
|
|
110
|
+
case 'initialize': {
|
|
111
|
+
const params = (request.params ?? {}) as { protocolVersion?: unknown };
|
|
112
|
+
/**
|
|
113
|
+
* Echo the client's version when it is a string, otherwise offer ours.
|
|
114
|
+
*
|
|
115
|
+
* A client that speaks a version this server has never heard of is still
|
|
116
|
+
* better served by being told what it asked for than by a hard refusal:
|
|
117
|
+
* every method below is version-independent, and the alternative is
|
|
118
|
+
* refusing to start over a string.
|
|
119
|
+
*/
|
|
120
|
+
const version =
|
|
121
|
+
typeof params.protocolVersion === 'string' ? params.protocolVersion : PROTOCOL_VERSION;
|
|
122
|
+
return {
|
|
123
|
+
jsonrpc: '2.0',
|
|
124
|
+
id,
|
|
125
|
+
result: {
|
|
126
|
+
protocolVersion: version,
|
|
127
|
+
// Tools only. Declaring capabilities this server does not implement is
|
|
128
|
+
// how a client comes to ask for one and get an error it did not expect.
|
|
129
|
+
capabilities: { tools: {} },
|
|
130
|
+
serverInfo: { name: info.name, version: info.version },
|
|
131
|
+
instructions: info.instructions,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'ping':
|
|
137
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
138
|
+
|
|
139
|
+
case 'tools/list':
|
|
140
|
+
return {
|
|
141
|
+
jsonrpc: '2.0',
|
|
142
|
+
id,
|
|
143
|
+
result: {
|
|
144
|
+
tools: tools.map((tool) => ({
|
|
145
|
+
name: tool.name,
|
|
146
|
+
title: tool.title,
|
|
147
|
+
description: tool.description,
|
|
148
|
+
inputSchema: tool.inputSchema,
|
|
149
|
+
})),
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
case 'tools/call': {
|
|
154
|
+
const params = (request.params ?? {}) as { name?: unknown; arguments?: unknown };
|
|
155
|
+
if (typeof params.name !== 'string') {
|
|
156
|
+
return errorFor(id, ERROR.invalidParams, 'name must be a string');
|
|
157
|
+
}
|
|
158
|
+
const tool = tools.find((candidate) => candidate.name === params.name);
|
|
159
|
+
if (!tool) {
|
|
160
|
+
return errorFor(id, ERROR.invalidParams, `unknown tool: ${params.name}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const args =
|
|
164
|
+
typeof params.arguments === 'object' && params.arguments !== null
|
|
165
|
+
? (params.arguments as Record<string, unknown>)
|
|
166
|
+
: {};
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
return {
|
|
170
|
+
jsonrpc: '2.0',
|
|
171
|
+
id,
|
|
172
|
+
result: { content: [{ type: 'text', text: tool.run(args) }] },
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
/**
|
|
176
|
+
* A tool failure is a *result* with `isError`, not a JSON-RPC error.
|
|
177
|
+
*
|
|
178
|
+
* The distinction is the whole point of that flag: a protocol error means
|
|
179
|
+
* the client is broken, while `isError` means the model asked for
|
|
180
|
+
* something it should ask differently, and the model is the one that
|
|
181
|
+
* needs to read the message. Sending a protocol error here would hide the
|
|
182
|
+
* explanation from the only party able to act on it.
|
|
183
|
+
*/
|
|
184
|
+
const text =
|
|
185
|
+
error instanceof InvalidArguments
|
|
186
|
+
? error.message
|
|
187
|
+
: `the tool failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
188
|
+
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
default:
|
|
193
|
+
return errorFor(
|
|
194
|
+
id,
|
|
195
|
+
ERROR.methodNotFound,
|
|
196
|
+
`this server implements tools only: ${request.method}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function errorFor(
|
|
202
|
+
id: string | number | null,
|
|
203
|
+
code: number,
|
|
204
|
+
message: string,
|
|
205
|
+
): object {
|
|
206
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Reads newline-delimited JSON from a stream and writes replies to another.
|
|
211
|
+
*
|
|
212
|
+
* Line-delimited rather than the Content-Length framing LSP uses, because that is
|
|
213
|
+
* what MCP's stdio transport specifies. A message may not contain a raw newline,
|
|
214
|
+
* which `JSON.stringify` guarantees.
|
|
215
|
+
*/
|
|
216
|
+
export function serve(
|
|
217
|
+
input: NodeJS.ReadableStream,
|
|
218
|
+
output: NodeJS.WritableStream,
|
|
219
|
+
tools: readonly ToolDefinition[],
|
|
220
|
+
info: ServerInfo,
|
|
221
|
+
): void {
|
|
222
|
+
let buffer = '';
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A cap, because the peer is not necessarily well behaved.
|
|
226
|
+
*
|
|
227
|
+
* Without one, a stream that never sends a newline grows this string until the
|
|
228
|
+
* process dies of memory exhaustion — a denial of service that needs no
|
|
229
|
+
* malice, just a client with a bug.
|
|
230
|
+
*/
|
|
231
|
+
const MAX_LINE = 8 * 1024 * 1024;
|
|
232
|
+
|
|
233
|
+
input.setEncoding('utf8');
|
|
234
|
+
input.on('data', (chunk: string) => {
|
|
235
|
+
buffer += chunk;
|
|
236
|
+
if (buffer.length > MAX_LINE) {
|
|
237
|
+
buffer = '';
|
|
238
|
+
write(output, errorFor(null, ERROR.invalidRequest, 'message exceeded 8 MiB'));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let newline = buffer.indexOf('\n');
|
|
243
|
+
while (newline !== -1) {
|
|
244
|
+
const line = buffer.slice(0, newline).trim();
|
|
245
|
+
buffer = buffer.slice(newline + 1);
|
|
246
|
+
if (line !== '') {
|
|
247
|
+
let decoded: unknown;
|
|
248
|
+
try {
|
|
249
|
+
decoded = JSON.parse(line);
|
|
250
|
+
} catch {
|
|
251
|
+
write(output, errorFor(null, ERROR.parse, 'not valid JSON'));
|
|
252
|
+
newline = buffer.indexOf('\n');
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const response = handle(decoded, tools, info);
|
|
256
|
+
if (response !== null) write(output, response);
|
|
257
|
+
}
|
|
258
|
+
newline = buffer.indexOf('\n');
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function write(output: NodeJS.WritableStream, message: object): void {
|
|
264
|
+
output.write(`${JSON.stringify(message)}\n`);
|
|
265
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUNDLED_CATALOGUE,
|
|
3
|
+
PRICING_LAST_REVIEWED,
|
|
4
|
+
formatUsd,
|
|
5
|
+
listModels,
|
|
6
|
+
optimize,
|
|
7
|
+
} from '@trazum/core';
|
|
8
|
+
import type { RuleLevel } from '@trazum/core';
|
|
9
|
+
|
|
10
|
+
import { InvalidArguments } from './rpc.js';
|
|
11
|
+
import type { ToolDefinition } from './rpc.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The tools, kept in one file so the whole surface an agent can reach reads in one
|
|
15
|
+
* pass.
|
|
16
|
+
*
|
|
17
|
+
* **Three deliberate absences, and they are the security design.**
|
|
18
|
+
*
|
|
19
|
+
* *No paths.* Every tool takes prompt text. A tool that accepted a filename would
|
|
20
|
+
* be a file-read primitive reachable by whatever the model decided to ask for, and
|
|
21
|
+
* "we reviewed it" is not a durable defence against one being added later. This
|
|
22
|
+
* package imports `@trazum/core`, the browser-safe entry point, and never
|
|
23
|
+
* `@trazum/core/node` — so the capability is *absent* rather than unused, and a
|
|
24
|
+
* test enforces that.
|
|
25
|
+
*
|
|
26
|
+
* *No network.* Nothing here calls a model. `--suggest` and `eval` exist in the
|
|
27
|
+
* CLI and are deliberately not exposed: they spend the caller's money, and a tool
|
|
28
|
+
* an agent can invoke in a loop must not be able to do that. Everything below is
|
|
29
|
+
* arithmetic on text.
|
|
30
|
+
*
|
|
31
|
+
* *No writes.* The tools return figures. Applying them is the agent's job, in its
|
|
32
|
+
* own context, where a human can see the diff.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The same cap the web API uses, for the same reason.
|
|
37
|
+
*
|
|
38
|
+
* An agent in a loop is exactly the caller that hands you a 40 MB string by
|
|
39
|
+
* accident. Refusing early with a number beats an unbounded pass over it.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_PROMPT_CHARS = 400_000;
|
|
42
|
+
|
|
43
|
+
/** Every figure this server prints descends from the estimator, so it says so. */
|
|
44
|
+
const BAND_NOTE =
|
|
45
|
+
'token counts are estimates (±15% on prose, calibrated on Claude); prices reviewed '
|
|
46
|
+
+ PRICING_LAST_REVIEWED;
|
|
47
|
+
|
|
48
|
+
function promptFrom(args: Record<string, unknown>): string {
|
|
49
|
+
const prompt = args.prompt;
|
|
50
|
+
if (typeof prompt !== 'string') throw new InvalidArguments('prompt must be a string');
|
|
51
|
+
if (prompt.length === 0) throw new InvalidArguments('prompt is empty');
|
|
52
|
+
if (prompt.length > MAX_PROMPT_CHARS) {
|
|
53
|
+
throw new InvalidArguments(
|
|
54
|
+
`prompt is ${prompt.length} characters, over the ${MAX_PROMPT_CHARS} limit`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return prompt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function levelFrom(args: Record<string, unknown>): RuleLevel {
|
|
61
|
+
const level = args.level ?? 'safe';
|
|
62
|
+
if (level !== 'safe' && level !== 'aggressive') {
|
|
63
|
+
throw new InvalidArguments('level must be "safe" or "aggressive"');
|
|
64
|
+
}
|
|
65
|
+
return level;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A positive integer, or the default.
|
|
70
|
+
*
|
|
71
|
+
* Written out rather than reached for from a validation library, and bounded on
|
|
72
|
+
* both ends: `callsPerMonth: 1e308` would otherwise produce an Infinity in
|
|
73
|
+
* somebody's budget, which is worse than a refusal because it looks like an
|
|
74
|
+
* answer.
|
|
75
|
+
*/
|
|
76
|
+
function intFrom(
|
|
77
|
+
args: Record<string, unknown>,
|
|
78
|
+
key: string,
|
|
79
|
+
fallback: number,
|
|
80
|
+
{ min, max }: { min: number; max: number },
|
|
81
|
+
): number {
|
|
82
|
+
const raw = args[key];
|
|
83
|
+
if (raw === undefined) return fallback;
|
|
84
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw)) {
|
|
85
|
+
throw new InvalidArguments(`${key} must be an integer`);
|
|
86
|
+
}
|
|
87
|
+
if (raw < min || raw > max) {
|
|
88
|
+
throw new InvalidArguments(`${key} must be between ${min} and ${max}`);
|
|
89
|
+
}
|
|
90
|
+
return raw;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const PROMPT_PROPERTY = {
|
|
94
|
+
type: 'string',
|
|
95
|
+
minLength: 1,
|
|
96
|
+
maxLength: MAX_PROMPT_CHARS,
|
|
97
|
+
description: 'The prompt text itself. This server never reads files.',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const LEVEL_PROPERTY = {
|
|
101
|
+
type: 'string',
|
|
102
|
+
enum: ['safe', 'aggressive'],
|
|
103
|
+
default: 'safe',
|
|
104
|
+
description: 'safe leaves meaning untouched; aggressive also rewords, and wants reading',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const OPTIMIZE: ToolDefinition = {
|
|
108
|
+
name: 'optimize_prompt',
|
|
109
|
+
title: 'Optimise a prompt and price the difference',
|
|
110
|
+
description:
|
|
111
|
+
"Applies Trazum's deterministic rules to a prompt and returns the shorter text, the "
|
|
112
|
+
+ 'token counts either side, what the difference is worth per month, and any advisories. '
|
|
113
|
+
+ 'Offline and free: no model is called.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
prompt: PROMPT_PROPERTY,
|
|
118
|
+
level: LEVEL_PROPERTY,
|
|
119
|
+
model: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
default: 'claude-opus-5',
|
|
122
|
+
description: 'Model id used for pricing. Call list_models for what is known.',
|
|
123
|
+
},
|
|
124
|
+
callsPerMonth: {
|
|
125
|
+
type: 'integer',
|
|
126
|
+
minimum: 1,
|
|
127
|
+
maximum: 1_000_000_000,
|
|
128
|
+
default: 1000,
|
|
129
|
+
description: 'Used only to scale the figures',
|
|
130
|
+
},
|
|
131
|
+
avgOutputTokens: { type: 'integer', minimum: 0, maximum: 1_000_000, default: 500 },
|
|
132
|
+
},
|
|
133
|
+
required: ['prompt'],
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
},
|
|
136
|
+
run: (args) => {
|
|
137
|
+
const model = args.model ?? 'claude-opus-5';
|
|
138
|
+
if (typeof model !== 'string') throw new InvalidArguments('model must be a string');
|
|
139
|
+
const callsPerMonth = intFrom(args, 'callsPerMonth', 1000, { min: 1, max: 1_000_000_000 });
|
|
140
|
+
const avgOutputTokens = intFrom(args, 'avgOutputTokens', 500, { min: 0, max: 1_000_000 });
|
|
141
|
+
|
|
142
|
+
const result = optimize(promptFrom(args), {
|
|
143
|
+
level: levelFrom(args),
|
|
144
|
+
usage: { model, callsPerMonth, avgOutputTokens },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const lines = [
|
|
148
|
+
`tokens: ${result.tokensBefore} → ${result.tokensAfter}`
|
|
149
|
+
+ ` (${result.tokensBefore - result.tokensAfter} fewer)`,
|
|
150
|
+
`monthly saving at ${callsPerMonth.toLocaleString('en-US')} calls:`
|
|
151
|
+
+ ` ${formatUsd(result.savings.monthlySavingsUsd)}`,
|
|
152
|
+
BAND_NOTE,
|
|
153
|
+
'',
|
|
154
|
+
'--- optimised prompt ---',
|
|
155
|
+
result.optimized,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
if (result.advisories.length > 0) {
|
|
159
|
+
lines.push('', '--- advisories ---');
|
|
160
|
+
for (const advisory of result.advisories) {
|
|
161
|
+
const money =
|
|
162
|
+
advisory.estimatedMonthlyUsd === null
|
|
163
|
+
? ''
|
|
164
|
+
: ` (~${formatUsd(advisory.estimatedMonthlyUsd)}/month)`;
|
|
165
|
+
lines.push(`[${advisory.id}] ${advisory.title}${money}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const CHECK: ToolDefinition = {
|
|
174
|
+
name: 'check_prompt',
|
|
175
|
+
title: 'Check a prompt against a token budget',
|
|
176
|
+
description:
|
|
177
|
+
'Answers whether a prompt fits a maximum, and if not, whether optimising it would. '
|
|
178
|
+
+ 'This is the one to call before sending a prompt you are unsure about.',
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
prompt: PROMPT_PROPERTY,
|
|
183
|
+
maxTokens: {
|
|
184
|
+
type: 'integer',
|
|
185
|
+
minimum: 1,
|
|
186
|
+
description: 'The budget. Required: a check with no maximum is not a check.',
|
|
187
|
+
},
|
|
188
|
+
level: LEVEL_PROPERTY,
|
|
189
|
+
},
|
|
190
|
+
required: ['prompt', 'maxTokens'],
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
},
|
|
193
|
+
run: (args) => {
|
|
194
|
+
if (args.maxTokens === undefined) throw new InvalidArguments('maxTokens is required');
|
|
195
|
+
const maxTokens = intFrom(args, 'maxTokens', 0, { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
196
|
+
const level = levelFrom(args);
|
|
197
|
+
const result = optimize(promptFrom(args), { level });
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Three outcomes, not two, and the third is why this tool exists.
|
|
201
|
+
*
|
|
202
|
+
* "Over budget" and "over budget but the rules would fix it" are different
|
|
203
|
+
* instructions to whoever asked: one means cut content, the other means run
|
|
204
|
+
* the rules. A boolean throws away the actionable half.
|
|
205
|
+
*/
|
|
206
|
+
const verdict =
|
|
207
|
+
result.tokensBefore <= maxTokens
|
|
208
|
+
? `PASS — ${result.tokensBefore} tokens, budget ${maxTokens}`
|
|
209
|
+
: result.tokensAfter <= maxTokens
|
|
210
|
+
? `OVER BUDGET — ${result.tokensBefore} tokens against ${maxTokens}, but the ${level}`
|
|
211
|
+
+ ` rules bring it to ${result.tokensAfter}, which fits. Optimise rather than cut.`
|
|
212
|
+
: `OVER BUDGET — ${result.tokensBefore} tokens against ${maxTokens}. Even optimised it`
|
|
213
|
+
+ ` is ${result.tokensAfter}: content has to be cut.`;
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
verdict,
|
|
217
|
+
'token counts are estimates (±15% on prose, calibrated on Claude), so a prompt within'
|
|
218
|
+
+ ' a few percent of its budget should be treated as uncertain',
|
|
219
|
+
].join('\n');
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const MODELS: ToolDefinition = {
|
|
224
|
+
name: 'list_models',
|
|
225
|
+
title: 'Models Trazum can price, and their rates',
|
|
226
|
+
description:
|
|
227
|
+
'Input and output price per million tokens, context window and cacheable minimum, for '
|
|
228
|
+
+ 'every model in the bundled catalogue.',
|
|
229
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
230
|
+
run: () => {
|
|
231
|
+
const rows = listModels().map((model) => {
|
|
232
|
+
const cache =
|
|
233
|
+
model.caching === 'none' || model.cacheMinTokens === null
|
|
234
|
+
? 'no caching'
|
|
235
|
+
: `cache min ${model.cacheMinTokens}`;
|
|
236
|
+
return `${model.id} in $${model.inputPerMTok}/Mtok out $${model.outputPerMTok}/Mtok`
|
|
237
|
+
+ ` context ${model.contextWindow.toLocaleString('en-US')} ${cache}`;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return [
|
|
241
|
+
`prices reviewed ${BUNDLED_CATALOGUE.lastReviewed} — verify before budgeting`,
|
|
242
|
+
'',
|
|
243
|
+
...rows,
|
|
244
|
+
].join('\n');
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** The whole surface. An exact list, asserted as one by the tests. */
|
|
249
|
+
export const TOOLS: readonly ToolDefinition[] = [OPTIMIZE, CHECK, MODELS];
|