fettle-mcp 0.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.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Execute a tool call as a real HTTP request against the public API.
3
+ *
4
+ * The central decision of S2, held identically in both channels: an MCP tool
5
+ * call is a transport adapter, not a second implementation of the API. This
6
+ * module maps a `ToolEntry` plus the model's arguments onto an ordinary
7
+ * `/v1/...` request carrying the user's key as a Bearer token. Authentication,
8
+ * the `x-fettle-public-api` scope marker, the per-organization rate bucket,
9
+ * pydantic validation and pagination all execute in core, unchanged, because
10
+ * it is a real request hitting the real route.
11
+ *
12
+ * Where core's executor dispatches in-process through `httpx.ASGITransport`,
13
+ * this one goes over the network with `fetch` — but the argument-routing rules
14
+ * and the success/failure boundary are deliberately the same, so a tool
15
+ * behaves identically whichever channel a user picked.
16
+ */
17
+ import { describeFailure, describeTransportFailure } from "./errors.js";
18
+ /**
19
+ * An argument the model can fix: missing, wrongly typed, or aimed at a
20
+ * parameter this tool does not declare. Surfaces as an error *tool result*, so
21
+ * the model reads the explanation and retries.
22
+ */
23
+ export class ToolArgumentError extends Error {
24
+ }
25
+ /**
26
+ * A failure the model cannot fix by retrying with different arguments — the
27
+ * API answered 5xx, or the request never got there. Surfaces as a JSON-RPC
28
+ * error, the same choice core makes for a 5xx.
29
+ */
30
+ export class ToolProtocolError extends Error {
31
+ }
32
+ /**
33
+ * Status codes at or above this boundary are protocol-level failures (the API
34
+ * itself is broken), not something the calling model can act on.
35
+ */
36
+ const SERVER_ERROR_THRESHOLD = 500;
37
+ function substitutePathParams(tool, args) {
38
+ let path = tool.operation.path;
39
+ for (const name of tool.operation.path_params) {
40
+ if (!(name in args)) {
41
+ throw new ToolArgumentError(`Tool '${tool.name}' requires argument '${name}'.`);
42
+ }
43
+ const raw = args[name];
44
+ delete args[name];
45
+ // A boolean is not a sane identifier even though JavaScript would happily
46
+ // stringify it — reject it explicitly rather than send "true" as an id.
47
+ if (typeof raw === "boolean" ||
48
+ (typeof raw !== "string" && typeof raw !== "number")) {
49
+ throw new ToolArgumentError(`Tool '${tool.name}' argument '${name}' must be a string or number; ` +
50
+ `got ${raw === null ? "null" : typeof raw}.`);
51
+ }
52
+ const value = String(raw);
53
+ // An empty value changes which route answers exactly the way a stray '/'
54
+ // does: DELETE /v1/monitors/ is not the route DELETE
55
+ // /v1/monitors/{monitor_id}, and the redirect it earns would be reported
56
+ // as something other than the delete the model asked for.
57
+ if (value === "") {
58
+ throw new ToolArgumentError(`Tool '${tool.name}' argument '${name}' must not be empty.`);
59
+ }
60
+ // A raw '/' or '?' would add a path segment or open a query string of its
61
+ // own. Percent-encoding is applied below anyway, but a value containing
62
+ // either is refused outright rather than quietly retargeted at a
63
+ // different route — the same rule core's executor enforces.
64
+ if (value.includes("/") || value.includes("?")) {
65
+ throw new ToolArgumentError(`Tool '${tool.name}' argument '${name}' may not contain '/' or '?'.`);
66
+ }
67
+ path = path.replace(`{${name}}`, encodeURIComponent(value));
68
+ }
69
+ return path;
70
+ }
71
+ function buildQuery(tool, args) {
72
+ const params = new URLSearchParams();
73
+ for (const name of tool.operation.query) {
74
+ if (!(name in args)) {
75
+ continue;
76
+ }
77
+ const value = args[name];
78
+ delete args[name];
79
+ if (value === undefined || value === null) {
80
+ continue;
81
+ }
82
+ // Arrays become repeated parameters (`regions=eu&regions=us`), which is
83
+ // how FastAPI reads a list query parameter — a single comma-joined value
84
+ // would arrive as one malformed member.
85
+ if (Array.isArray(value)) {
86
+ for (const item of value) {
87
+ params.append(name, typeof item === "boolean" ? String(item) : String(item));
88
+ }
89
+ continue;
90
+ }
91
+ params.append(name, String(value));
92
+ }
93
+ return params;
94
+ }
95
+ async function readBody(response) {
96
+ const text = await response.text();
97
+ if (text === "") {
98
+ return undefined;
99
+ }
100
+ try {
101
+ return JSON.parse(text);
102
+ }
103
+ catch {
104
+ // Not JSON at all — a proxy's HTML error page, say. Returning the raw text
105
+ // is more useful to the reader than pretending there was no body.
106
+ return text;
107
+ }
108
+ }
109
+ /** Dispatches `tools/call` invocations as requests against the public API. */
110
+ export class ToolExecutor {
111
+ options;
112
+ fetchImpl;
113
+ constructor(options) {
114
+ this.options = options;
115
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
116
+ }
117
+ /**
118
+ * Execute one tool call and return an MCP `tools/call` result.
119
+ *
120
+ * Flow:
121
+ * 1. Substitute the operation's declared path parameters into the URL
122
+ * template, percent-encoding each value so it cannot introduce extra path
123
+ * segments or a query string of its own.
124
+ * 2. Route the declared query parameters into the query string.
125
+ * 3. Route whatever arguments remain into the JSON body when the operation
126
+ * declares one; otherwise an argument that matched no declared parameter
127
+ * is rejected rather than silently dropped.
128
+ * 4. Dispatch the request with the user's key as a Bearer token, following
129
+ * no redirects: a 3xx is reported, never chased, so the credential cannot
130
+ * be replayed to a host the contract did not name.
131
+ * 5. Map the response — 2xx is a result, any other non-5xx is an actionable
132
+ * tool error, 5xx and transport failures raise.
133
+ *
134
+ * Throws:
135
+ * ToolArgumentError: an argument is missing, wrongly typed, or matched no
136
+ * declared parameter on a tool with no request body.
137
+ * ToolProtocolError: the API answered 5xx, or the request never completed.
138
+ */
139
+ async execute(tool, args) {
140
+ const remaining = { ...args };
141
+ const path = substitutePathParams(tool, remaining);
142
+ const query = buildQuery(tool, remaining);
143
+ let body;
144
+ if (tool.operation.body) {
145
+ body = JSON.stringify(remaining);
146
+ }
147
+ else if (Object.keys(remaining).length > 0) {
148
+ const unexpected = Object.keys(remaining).sort().join(", ");
149
+ throw new ToolArgumentError(`Tool '${tool.name}' does not accept argument(s): ${unexpected}.`);
150
+ }
151
+ const url = `${this.options.apiUrl}${path}${query.size > 0 ? `?${query}` : ""}`;
152
+ const headers = {
153
+ Authorization: `Bearer ${this.options.apiKey}`,
154
+ Accept: "application/json",
155
+ "User-Agent": this.options.userAgent,
156
+ };
157
+ if (body !== undefined) {
158
+ headers["Content-Type"] = "application/json";
159
+ }
160
+ let response;
161
+ try {
162
+ response = await this.fetchImpl(url, {
163
+ method: tool.operation.method,
164
+ headers,
165
+ ...(body === undefined ? {} : { body }),
166
+ redirect: "manual",
167
+ signal: AbortSignal.timeout(this.options.timeoutMs),
168
+ });
169
+ }
170
+ catch (cause) {
171
+ if (cause instanceof Error && cause.name === "TimeoutError") {
172
+ throw new ToolProtocolError(`Tool '${tool.name}' timed out after ${this.options.timeoutMs}ms ` +
173
+ `waiting for ${this.options.apiUrl}. Retry, or raise ` +
174
+ "FETTLE_TIMEOUT_MS if this endpoint is legitimately slow.");
175
+ }
176
+ throw new ToolProtocolError(describeTransportFailure(this.options.apiUrl, cause));
177
+ }
178
+ if (response.status >= SERVER_ERROR_THRESHOLD) {
179
+ throw new ToolProtocolError(`Tool '${tool.name}' failed: the API returned ${response.status}.`);
180
+ }
181
+ const payload = await readBody(response);
182
+ if (response.status < 200 || response.status >= 300) {
183
+ const errorBody = typeof payload === "object" && payload !== null && !Array.isArray(payload)
184
+ ? payload
185
+ : null;
186
+ return {
187
+ content: [
188
+ {
189
+ type: "text",
190
+ text: describeFailure(response.status, errorBody, response.headers),
191
+ },
192
+ ],
193
+ isError: true,
194
+ };
195
+ }
196
+ const text = payload === undefined
197
+ ? `${tool.name} succeeded (${response.status}); the response had no body.`
198
+ : JSON.stringify(payload);
199
+ return { content: [{ type: "text", text }], isError: false };
200
+ }
201
+ }
202
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npx fettle-mcp` — the stdio MCP server for Fettle.
4
+ *
5
+ * Everything that can be wrong with the setup is checked here, before the
6
+ * transport is connected: a missing key, an unusable API URL, a contract this
7
+ * version cannot read. The reason is the medium — an MCP client spawns this
8
+ * process with stdout reserved for JSON-RPC frames and nothing but stderr for
9
+ * a human to read. A configuration mistake discovered later, inside a tool
10
+ * call, reaches the user as "the assistant could not check your monitor",
11
+ * which is the wrong diagnosis of the wrong problem.
12
+ */
13
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npx fettle-mcp` — the stdio MCP server for Fettle.
4
+ *
5
+ * Everything that can be wrong with the setup is checked here, before the
6
+ * transport is connected: a missing key, an unusable API URL, a contract this
7
+ * version cannot read. The reason is the medium — an MCP client spawns this
8
+ * process with stdout reserved for JSON-RPC frames and nothing but stderr for
9
+ * a human to read. A configuration mistake discovered later, inside a tool
10
+ * call, reaches the user as "the assistant could not check your monitor",
11
+ * which is the wrong diagnosis of the wrong problem.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { ConfigError, loadConfig } from "./config.js";
16
+ import { loadContract } from "./contract.js";
17
+ import { ToolExecutor } from "./executor.js";
18
+ import { createServer } from "./server.js";
19
+ const SERVER_NAME = "fettle-mcp";
20
+ /**
21
+ * The published version, read from the package manifest at startup.
22
+ *
23
+ * Reported to the MCP client in `serverInfo` and sent as the `User-Agent` on
24
+ * every API call, so a support question about odd behaviour can be answered
25
+ * with "which version" from the server's own logs. `src/` and `dist/` both sit
26
+ * one level under the package root, so this path is right compiled and under
27
+ * test alike.
28
+ */
29
+ function readVersion() {
30
+ try {
31
+ const manifestUrl = new URL("../package.json", import.meta.url);
32
+ const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
33
+ return manifest.version ?? "0.0.0";
34
+ }
35
+ catch {
36
+ return "0.0.0";
37
+ }
38
+ }
39
+ async function main() {
40
+ const config = loadConfig();
41
+ const contract = loadContract();
42
+ const version = readVersion();
43
+ const executor = new ToolExecutor({
44
+ apiUrl: config.apiUrl,
45
+ apiKey: config.apiKey,
46
+ timeoutMs: config.timeoutMs,
47
+ userAgent: `${SERVER_NAME}/${version} (+https://fettle.sh)`,
48
+ });
49
+ const server = createServer(contract, executor, {
50
+ name: SERVER_NAME,
51
+ version,
52
+ });
53
+ await server.connect(new StdioServerTransport());
54
+ // Nothing is logged on success: stdout carries the protocol, and a startup
55
+ // banner on stderr shows up in some clients as an error the user must chase.
56
+ }
57
+ main().catch((error) => {
58
+ const message = error instanceof ConfigError || error instanceof Error
59
+ ? error.message
60
+ : String(error);
61
+ process.stderr.write(`fettle-mcp: ${message}\n`);
62
+ process.exit(1);
63
+ });
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC;;;;;;;;GAQG;AACH,SAAS,WAAW;IAClB,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAE5D,CAAC;QACF,OAAO,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAE9B,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC;QAChC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,GAAG,WAAW,IAAI,OAAO,uBAAuB;KAC5D,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE;QAC9C,IAAI,EAAE,WAAW;QACjB,OAAO;KACR,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,2EAA2E;IAC3E,6EAA6E;AAC/E,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,MAAM,OAAO,GACX,KAAK,YAAY,WAAW,IAAI,KAAK,YAAY,KAAK;QACpD,CAAC,CAAC,KAAK,CAAC,OAAO;QACf,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,OAAO,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Wire the contract, the validators and the executor into an MCP server.
3
+ *
4
+ * The server registers no tool by hand. `tools/list` is the contract rendered
5
+ * into MCP's wire shape, and `tools/call` is "find the entry, validate against
6
+ * its schema, execute it" — so a tool added in core and re-vendored here
7
+ * appears in the package without a line of source changing. That is the whole
8
+ * point of generating the contract in the first place.
9
+ */
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { type ToolContract } from "./contract.js";
12
+ import { type ToolExecutor } from "./executor.js";
13
+ export interface ServerIdentity {
14
+ name: string;
15
+ version: string;
16
+ }
17
+ /**
18
+ * Build the MCP server.
19
+ *
20
+ * The failure surface is split deliberately, matching core's remote channel:
21
+ * anything the model can fix by calling again with different arguments comes
22
+ * back as an error *result* it can read and act on, while anything it cannot
23
+ * — an unknown tool name, a 5xx, an unreachable API — is raised as a protocol
24
+ * error. Dressing a broken backend up as "here is what to fix" would send the
25
+ * model into a retry loop against a problem no argument can solve.
26
+ */
27
+ export declare function createServer(contract: ToolContract, executor: ToolExecutor, identity: ServerIdentity): Server;
package/dist/server.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Wire the contract, the validators and the executor into an MCP server.
3
+ *
4
+ * The server registers no tool by hand. `tools/list` is the contract rendered
5
+ * into MCP's wire shape, and `tools/call` is "find the entry, validate against
6
+ * its schema, execute it" — so a tool added in core and re-vendored here
7
+ * appears in the package without a line of source changing. That is the whole
8
+ * point of generating the contract in the first place.
9
+ */
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
12
+ import { findTool } from "./contract.js";
13
+ import { ToolArgumentError, ToolProtocolError } from "./executor.js";
14
+ import { compileValidators, describeSchemaErrors } from "./validation.js";
15
+ /**
16
+ * Render the contract as MCP tool descriptors.
17
+ *
18
+ * `inputSchema` is the wire-format camelCase key; the contract stores the same
19
+ * data under the snake_case `input_schema` it was generated with. The schema
20
+ * is passed through untouched — this package must not develop a second opinion
21
+ * about what a tool's arguments look like.
22
+ */
23
+ function toWireTools(contract) {
24
+ return contract.tools.map((tool) => ({
25
+ name: tool.name,
26
+ title: tool.title,
27
+ description: tool.description,
28
+ inputSchema: tool.input_schema,
29
+ }));
30
+ }
31
+ /**
32
+ * Build the MCP server.
33
+ *
34
+ * The failure surface is split deliberately, matching core's remote channel:
35
+ * anything the model can fix by calling again with different arguments comes
36
+ * back as an error *result* it can read and act on, while anything it cannot
37
+ * — an unknown tool name, a 5xx, an unreachable API — is raised as a protocol
38
+ * error. Dressing a broken backend up as "here is what to fix" would send the
39
+ * model into a retry loop against a problem no argument can solve.
40
+ */
41
+ export function createServer(contract, executor, identity) {
42
+ const validators = compileValidators(contract);
43
+ const wireTools = toWireTools(contract);
44
+ const server = new Server(identity, { capabilities: { tools: {} } });
45
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: wireTools }));
46
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
47
+ const { name, arguments: rawArguments } = request.params;
48
+ const tool = findTool(contract, name);
49
+ if (tool === undefined) {
50
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool '${name}'.`);
51
+ }
52
+ const args = (rawArguments ?? {});
53
+ const validate = validators.get(name);
54
+ if (validate !== undefined && !validate(args)) {
55
+ return {
56
+ content: [
57
+ { type: "text", text: describeSchemaErrors(name, validate.errors) },
58
+ ],
59
+ isError: true,
60
+ };
61
+ }
62
+ try {
63
+ return await executor.execute(tool, args);
64
+ }
65
+ catch (error) {
66
+ if (error instanceof ToolArgumentError) {
67
+ return { content: [{ type: "text", text: error.message }], isError: true };
68
+ }
69
+ if (error instanceof ToolProtocolError) {
70
+ throw new McpError(ErrorCode.InternalError, error.message);
71
+ }
72
+ throw error;
73
+ }
74
+ });
75
+ return server;
76
+ }
77
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Argument validation against the contract's own JSON Schemas.
3
+ *
4
+ * The schemas are generated from core's pydantic models, so validating here
5
+ * catches exactly what the API would have rejected — one round trip earlier,
6
+ * without spending a unit of the organization's rate budget, and with the
7
+ * offending field named in the same wording a 422 would have used. This is a
8
+ * pre-filter, never an authority: the API still validates everything, and a
9
+ * schema that lets something through is core's problem to answer.
10
+ *
11
+ * Ajv runs with `strict: false` on purpose. Strict mode rejects schemas over
12
+ * details of *how* they are written — an unrecognised keyword, a format it
13
+ * does not know — and this package does not author these schemas, it receives
14
+ * them. A newly generated contract must never fail to load here because the
15
+ * generator started emitting a keyword this version had not seen.
16
+ */
17
+ import { type ErrorObject, type ValidateFunction } from "ajv";
18
+ import type { ToolContract } from "./contract.js";
19
+ export type ToolValidators = Map<string, ValidateFunction>;
20
+ /** Compile one validator per tool, once, at startup. */
21
+ export declare function compileValidators(contract: ToolContract): ToolValidators;
22
+ /**
23
+ * Turn Ajv's errors into the same field-naming sentence a 422 would produce.
24
+ *
25
+ * Deduplicated because `allErrors` reports one entry per failing keyword, and
26
+ * a single bad value routinely trips two of them — the model does not need to
27
+ * be told twice about one argument.
28
+ */
29
+ export declare function describeSchemaErrors(toolName: string, errors: ErrorObject[] | null | undefined): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Argument validation against the contract's own JSON Schemas.
3
+ *
4
+ * The schemas are generated from core's pydantic models, so validating here
5
+ * catches exactly what the API would have rejected — one round trip earlier,
6
+ * without spending a unit of the organization's rate budget, and with the
7
+ * offending field named in the same wording a 422 would have used. This is a
8
+ * pre-filter, never an authority: the API still validates everything, and a
9
+ * schema that lets something through is core's problem to answer.
10
+ *
11
+ * Ajv runs with `strict: false` on purpose. Strict mode rejects schemas over
12
+ * details of *how* they are written — an unrecognised keyword, a format it
13
+ * does not know — and this package does not author these schemas, it receives
14
+ * them. A newly generated contract must never fail to load here because the
15
+ * generator started emitting a keyword this version had not seen.
16
+ */
17
+ import { Ajv } from "ajv";
18
+ import ajvFormats from "ajv-formats";
19
+ /**
20
+ * `ajv-formats` is CommonJS whose `module.exports` *is* the plugin function.
21
+ * Node's ESM interop hands that function straight to a default import, but
22
+ * TypeScript under NodeNext types the same import as the module namespace, so
23
+ * the shape is asserted once here rather than fought at the call site.
24
+ */
25
+ const addFormats = ajvFormats;
26
+ /** Compile one validator per tool, once, at startup. */
27
+ export function compileValidators(contract) {
28
+ const ajv = new Ajv({ allErrors: true, strict: false });
29
+ addFormats(ajv);
30
+ const validators = new Map();
31
+ for (const tool of contract.tools) {
32
+ validators.set(tool.name, ajv.compile(tool.input_schema));
33
+ }
34
+ return validators;
35
+ }
36
+ function describeError(error) {
37
+ const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
38
+ if (error.keyword === "required") {
39
+ const missing = error.params.missingProperty;
40
+ const field = path === "" ? missing : `${path}.${missing}`;
41
+ return `'${field}': required argument is missing`;
42
+ }
43
+ if (error.keyword === "additionalProperties") {
44
+ const extra = error.params.additionalProperty;
45
+ const field = path === "" ? extra : `${path}.${extra}`;
46
+ return `'${field}': unexpected argument — this tool does not accept it`;
47
+ }
48
+ const field = path === "" ? "arguments" : path;
49
+ return `'${field}': ${error.message ?? "is invalid"}`;
50
+ }
51
+ /**
52
+ * Turn Ajv's errors into the same field-naming sentence a 422 would produce.
53
+ *
54
+ * Deduplicated because `allErrors` reports one entry per failing keyword, and
55
+ * a single bad value routinely trips two of them — the model does not need to
56
+ * be told twice about one argument.
57
+ */
58
+ export function describeSchemaErrors(toolName, errors) {
59
+ const parts = [...new Set((errors ?? []).map(describeError))];
60
+ if (parts.length === 0) {
61
+ return `Tool '${toolName}' rejected the arguments. Check them and retry.`;
62
+ }
63
+ return `Invalid argument(s) for '${toolName}' — ${parts.join("; ")}. Fix the value(s) and retry.`;
64
+ }
65
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,GAAG,EAA2C,MAAM,KAAK,CAAC;AACnE,OAAO,UAAU,MAAM,aAAa,CAAC;AAMrC;;;;;GAKG;AACH,MAAM,UAAU,GAAG,UAA0C,CAAC;AAE9D,wDAAwD;AACxD,MAAM,UAAU,iBAAiB,CAAC,QAAsB;IACtD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,UAAU,CAAC,GAAG,CAAC,CAAC;IAChB,MAAM,UAAU,GAAmB,IAAI,GAAG,EAAE,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,aAAa,CAAC,KAAkB;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACvE,IAAI,KAAK,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,OAAO,GAAI,KAAK,CAAC,MAAuC,CAAC,eAAe,CAAC;QAC/E,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3D,OAAO,IAAI,KAAK,iCAAiC,CAAC;IACpD,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,sBAAsB,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAI,KAAK,CAAC,MAA0C,CAAC,kBAAkB,CAAC;QACnF,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC;QACvD,OAAO,IAAI,KAAK,uDAAuD,CAAC;IAC1E,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,OAAO,IAAI,KAAK,MAAM,KAAK,CAAC,OAAO,IAAI,YAAY,EAAE,CAAC;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAgB,EAChB,MAAwC;IAExC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,SAAS,QAAQ,iDAAiD,CAAC;IAC5E,CAAC;IACD,OAAO,4BAA4B,QAAQ,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,+BAA+B,CAAC;AACpG,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "fettle-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Fettle (fettle.sh) uptime monitoring — manage monitors, incidents, heartbeats and status pages from any MCP client.",
5
+ "keywords": [
6
+ "mcp",
7
+ "modelcontextprotocol",
8
+ "fettle",
9
+ "uptime",
10
+ "monitoring",
11
+ "claude",
12
+ "cursor"
13
+ ],
14
+ "homepage": "https://fettle.sh",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/darkweid/monitoring-mcp.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/darkweid/monitoring-mcp/issues"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Mark Shamanskii",
24
+ "type": "module",
25
+ "bin": {
26
+ "fettle-mcp": "dist/index.js"
27
+ },
28
+ "main": "dist/index.js",
29
+ "files": [
30
+ "dist",
31
+ "contracts/mcp_tools.json",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20.0.0"
37
+ },
38
+ "packageManager": "pnpm@10.15.1",
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check .",
46
+ "contract:check": "node scripts/check-contract-drift.mjs",
47
+ "prepublishOnly": "pnpm run build"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "1.30.0",
51
+ "ajv": "^8.17.1",
52
+ "ajv-formats": "^3.0.1"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.10.5",
56
+ "prettier": "^3.4.2",
57
+ "typescript": "^5.7.3",
58
+ "vitest": "^3.0.5"
59
+ }
60
+ }