@euclid-tools/euclid 0.2.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/.claude-plugin/plugin.json +15 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/index.js +465 -0
- package/hooks/hooks.json +16 -0
- package/hooks/run-hook.cmd +40 -0
- package/hooks/session-start +49 -0
- package/package.json +58 -0
- package/skills/math/EXPRESSIONS.md +103 -0
- package/skills/math/SKILL.md +93 -0
- package/skills/math/STATISTICS.md +51 -0
- package/skills/math/UNITS.md +130 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
|
|
8
|
+
// src/tools/calculate.ts
|
|
9
|
+
import { z } from "zod/v4";
|
|
10
|
+
|
|
11
|
+
// src/engine.ts
|
|
12
|
+
import { create, all } from "mathjs";
|
|
13
|
+
import vm from "vm";
|
|
14
|
+
|
|
15
|
+
// src/normalization.ts
|
|
16
|
+
var UNIT_ALIASES = {
|
|
17
|
+
celsius: "degC",
|
|
18
|
+
fahrenheit: "degF",
|
|
19
|
+
"kilometers per hour": "km/hour",
|
|
20
|
+
"miles per hour": "mile/hour",
|
|
21
|
+
"meters per second": "m/s",
|
|
22
|
+
"feet per second": "ft/s",
|
|
23
|
+
"square meters": "m^2",
|
|
24
|
+
"square feet": "ft^2",
|
|
25
|
+
"square kilometers": "km^2",
|
|
26
|
+
"square miles": "mile^2",
|
|
27
|
+
"cubic meters": "m^3",
|
|
28
|
+
"cubic feet": "ft^3",
|
|
29
|
+
"cubic inches": "in^3",
|
|
30
|
+
litres: "liter"
|
|
31
|
+
};
|
|
32
|
+
function normalizeUnit(input) {
|
|
33
|
+
const trimmed = input.trim();
|
|
34
|
+
const key = trimmed.toLowerCase();
|
|
35
|
+
const mapped = UNIT_ALIASES[key];
|
|
36
|
+
if (mapped) {
|
|
37
|
+
return { value: mapped, wasTransformed: true, original: input };
|
|
38
|
+
}
|
|
39
|
+
return { value: trimmed, wasTransformed: trimmed !== input, original: input };
|
|
40
|
+
}
|
|
41
|
+
var EXPRESSION_REPLACEMENTS = [
|
|
42
|
+
[/×/g, "*"],
|
|
43
|
+
[/÷/g, "/"],
|
|
44
|
+
[/²/g, "^2"],
|
|
45
|
+
[/³/g, "^3"],
|
|
46
|
+
[/√\(/g, "sqrt("],
|
|
47
|
+
[/√(\d+(?:\.\d+)?)/g, "sqrt($1)"],
|
|
48
|
+
[/\u2212/g, "-"],
|
|
49
|
+
[/π/g, "pi"]
|
|
50
|
+
];
|
|
51
|
+
function normalizeExpression(input) {
|
|
52
|
+
let value = input;
|
|
53
|
+
for (const [pattern, replacement] of EXPRESSION_REPLACEMENTS) {
|
|
54
|
+
value = value.replace(pattern, replacement);
|
|
55
|
+
}
|
|
56
|
+
value = value.replace(/\d{1,3}(?:,\d{3})+(?!\d)/g, (match) => match.replace(/,/g, ""));
|
|
57
|
+
return {
|
|
58
|
+
value,
|
|
59
|
+
wasTransformed: value !== input,
|
|
60
|
+
original: input
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/engine.ts
|
|
65
|
+
var MAX_EXPRESSION_LENGTH = 1e3;
|
|
66
|
+
var TIMEOUT_MS = 5e3;
|
|
67
|
+
var MAX_DATA_LENGTH = 1e4;
|
|
68
|
+
var math = create(all);
|
|
69
|
+
var limitedEvaluate = math.evaluate;
|
|
70
|
+
math.createUnit("mph", "1 mile/hour");
|
|
71
|
+
math.createUnit("kph", "1 km/hour");
|
|
72
|
+
math.createUnit("kmh", "1 km/hour");
|
|
73
|
+
math.createUnit("knot", "1.852 km/hour");
|
|
74
|
+
math.createUnit("knots", "1 knot");
|
|
75
|
+
math.createUnit("kn", "1 knot");
|
|
76
|
+
math.createUnit("kt", "1 knot");
|
|
77
|
+
math.createUnit("nmi", "1.852 km");
|
|
78
|
+
math.import(
|
|
79
|
+
{
|
|
80
|
+
import: () => {
|
|
81
|
+
throw new Error("Function import is disabled");
|
|
82
|
+
},
|
|
83
|
+
createUnit: () => {
|
|
84
|
+
throw new Error("Function createUnit is disabled");
|
|
85
|
+
},
|
|
86
|
+
evaluate: () => {
|
|
87
|
+
throw new Error("Function evaluate is disabled");
|
|
88
|
+
},
|
|
89
|
+
parse: () => {
|
|
90
|
+
throw new Error("Function parse is disabled");
|
|
91
|
+
},
|
|
92
|
+
simplify: () => {
|
|
93
|
+
throw new Error("Function simplify is disabled");
|
|
94
|
+
},
|
|
95
|
+
derivative: () => {
|
|
96
|
+
throw new Error("Function derivative is disabled");
|
|
97
|
+
},
|
|
98
|
+
resolve: () => {
|
|
99
|
+
throw new Error("Function resolve is disabled");
|
|
100
|
+
},
|
|
101
|
+
reviver: () => {
|
|
102
|
+
throw new Error("Function reviver is disabled");
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{ override: true }
|
|
106
|
+
);
|
|
107
|
+
function evaluateExpression(expression, precision = 14) {
|
|
108
|
+
if (expression.trim().length === 0) {
|
|
109
|
+
return { error: "Expression is empty" };
|
|
110
|
+
}
|
|
111
|
+
if (expression.length > MAX_EXPRESSION_LENGTH) {
|
|
112
|
+
return {
|
|
113
|
+
error: `Expression too long (${expression.length} chars, max ${MAX_EXPRESSION_LENGTH})`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const norm = normalizeExpression(expression);
|
|
117
|
+
try {
|
|
118
|
+
const sandbox = { fn: limitedEvaluate, expr: norm.value };
|
|
119
|
+
const raw = vm.runInNewContext("fn(expr)", sandbox, { timeout: TIMEOUT_MS });
|
|
120
|
+
const formatted = math.format(raw, { precision, upperExp: 14, lowerExp: -14 });
|
|
121
|
+
const engineResult = { result: formatted };
|
|
122
|
+
if (norm.wasTransformed) {
|
|
123
|
+
engineResult.note = `Expression '${norm.original}' was interpreted as '${norm.value}'`;
|
|
124
|
+
}
|
|
125
|
+
return engineResult;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
128
|
+
if (message.includes("Script execution timed out")) {
|
|
129
|
+
return { error: "Computation timed out after 5 seconds" };
|
|
130
|
+
}
|
|
131
|
+
return { error: message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function convertUnit(value, from, to) {
|
|
135
|
+
const normFrom = normalizeUnit(from);
|
|
136
|
+
const normTo = normalizeUnit(to);
|
|
137
|
+
try {
|
|
138
|
+
const unit = math.unit(value, normFrom.value);
|
|
139
|
+
const converted = unit.to(normTo.value);
|
|
140
|
+
const num = converted.toNumber();
|
|
141
|
+
const engineResult = { result: String(num) };
|
|
142
|
+
const notes = [];
|
|
143
|
+
if (normFrom.wasTransformed) {
|
|
144
|
+
notes.push(`'${normFrom.original}' was interpreted as '${normFrom.value}'`);
|
|
145
|
+
}
|
|
146
|
+
if (normTo.wasTransformed) {
|
|
147
|
+
notes.push(`'${normTo.original}' was interpreted as '${normTo.value}'`);
|
|
148
|
+
}
|
|
149
|
+
if (notes.length > 0) {
|
|
150
|
+
engineResult.note = notes.join("; ");
|
|
151
|
+
}
|
|
152
|
+
return engineResult;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
return { error: message };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function computeStatistic(operation, data, percentile) {
|
|
159
|
+
if (data.length === 0) {
|
|
160
|
+
return { error: "Data array is empty" };
|
|
161
|
+
}
|
|
162
|
+
if (data.length > MAX_DATA_LENGTH) {
|
|
163
|
+
return { error: `Data array too many elements (${data.length}, max ${MAX_DATA_LENGTH})` };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
let result;
|
|
167
|
+
switch (operation) {
|
|
168
|
+
case "mean":
|
|
169
|
+
result = math.mean(data);
|
|
170
|
+
break;
|
|
171
|
+
case "median":
|
|
172
|
+
result = math.median(data);
|
|
173
|
+
break;
|
|
174
|
+
case "mode":
|
|
175
|
+
result = math.mode(data);
|
|
176
|
+
result = Array.isArray(result) ? result[0] : result;
|
|
177
|
+
break;
|
|
178
|
+
case "std":
|
|
179
|
+
result = math.std(data);
|
|
180
|
+
break;
|
|
181
|
+
case "variance":
|
|
182
|
+
result = math.variance(data);
|
|
183
|
+
break;
|
|
184
|
+
case "min":
|
|
185
|
+
result = math.min(data);
|
|
186
|
+
break;
|
|
187
|
+
case "max":
|
|
188
|
+
result = math.max(data);
|
|
189
|
+
break;
|
|
190
|
+
case "sum":
|
|
191
|
+
result = math.sum(data);
|
|
192
|
+
break;
|
|
193
|
+
case "percentile":
|
|
194
|
+
if (percentile === void 0) {
|
|
195
|
+
return { error: 'Percentile value is required when operation is "percentile"' };
|
|
196
|
+
}
|
|
197
|
+
if (percentile < 0 || percentile > 100) {
|
|
198
|
+
return { error: "Percentile must be between 0 and 100" };
|
|
199
|
+
}
|
|
200
|
+
result = math.quantileSeq(data, percentile / 100);
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
return { error: `Unknown operation: ${operation}` };
|
|
204
|
+
}
|
|
205
|
+
return { result: String(result) };
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
208
|
+
return { error: message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/error-hints.ts
|
|
213
|
+
var CALCULATE_EXAMPLES = ["2 * 3", "sqrt(16)", "sin(pi / 4)", "log(100, 10)", "12! / (4! * 8!)"];
|
|
214
|
+
var CONVERT_EXAMPLES = [
|
|
215
|
+
"convert(5, 'km', 'mile')",
|
|
216
|
+
"convert(100, 'degF', 'degC')",
|
|
217
|
+
"convert(1, 'lb', 'kg')"
|
|
218
|
+
];
|
|
219
|
+
var STATISTICS_EXAMPLES = [
|
|
220
|
+
"statistics('mean', [1, 2, 3])",
|
|
221
|
+
"statistics('percentile', [10, 20, 30], 90)"
|
|
222
|
+
];
|
|
223
|
+
function getCalculateHint(errorMessage) {
|
|
224
|
+
if (errorMessage.includes("Unexpected") || errorMessage.includes("Parenthesis") || errorMessage.includes("Value expected")) {
|
|
225
|
+
return "Check expression syntax. Use * for multiplication, / for division, ^ for exponents, and ensure parentheses are balanced.";
|
|
226
|
+
}
|
|
227
|
+
if (errorMessage.includes("Undefined symbol") || errorMessage.includes("Undefined function")) {
|
|
228
|
+
return "Unknown variable or function. Supported functions include: sqrt, sin, cos, tan, log, exp, abs, ceil, floor, round.";
|
|
229
|
+
}
|
|
230
|
+
if (errorMessage.includes("is disabled")) {
|
|
231
|
+
return "This function is disabled for security. Use basic arithmetic and math functions only.";
|
|
232
|
+
}
|
|
233
|
+
return "Invalid expression. Use standard mathematical notation with operators: +, -, *, /, ^.";
|
|
234
|
+
}
|
|
235
|
+
function getConvertHint(errorMessage) {
|
|
236
|
+
if (errorMessage.includes("not found")) {
|
|
237
|
+
return "Unit not recognized. Use standard abbreviations: km, m, ft, mile, lb, kg, degC, degF, mph, kph.";
|
|
238
|
+
}
|
|
239
|
+
if (errorMessage.includes("do not match")) {
|
|
240
|
+
return "Units are incompatible. Ensure both measure the same quantity (e.g., length to length, weight to weight).";
|
|
241
|
+
}
|
|
242
|
+
return "Invalid conversion. Provide a numeric value with valid source and target units.";
|
|
243
|
+
}
|
|
244
|
+
function getStatisticsHint(errorMessage) {
|
|
245
|
+
if (errorMessage.includes("Unknown operation")) {
|
|
246
|
+
return "Valid operations: mean, median, mode, std, variance, min, max, sum, percentile.";
|
|
247
|
+
}
|
|
248
|
+
if (errorMessage.includes("Percentile")) {
|
|
249
|
+
return "The percentile parameter is required and must be between 0 and 100.";
|
|
250
|
+
}
|
|
251
|
+
if (errorMessage.includes("empty")) {
|
|
252
|
+
return "Data array must contain at least one number.";
|
|
253
|
+
}
|
|
254
|
+
return "Provide a valid operation and a non-empty array of numbers.";
|
|
255
|
+
}
|
|
256
|
+
function getErrorHint(tool, errorMessage) {
|
|
257
|
+
switch (tool) {
|
|
258
|
+
case "calculate":
|
|
259
|
+
return { hint: getCalculateHint(errorMessage), examples: CALCULATE_EXAMPLES };
|
|
260
|
+
case "convert":
|
|
261
|
+
return { hint: getConvertHint(errorMessage), examples: CONVERT_EXAMPLES };
|
|
262
|
+
case "statistics":
|
|
263
|
+
return { hint: getStatisticsHint(errorMessage), examples: STATISTICS_EXAMPLES };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/tools/calculate.ts
|
|
268
|
+
var calculateTool = {
|
|
269
|
+
name: "calculate",
|
|
270
|
+
description: `Deterministic calculator for mathematical expressions. Use this tool whenever you need to compute a numerical result rather than predict one. This includes: arithmetic operations, percentages, exponents, roots, trigonometry, logarithms, factorials, and any expression that has a single correct numerical answer.
|
|
271
|
+
|
|
272
|
+
DO NOT attempt to calculate results from memory or prediction. If a user asks a question that requires computation, use this tool.
|
|
273
|
+
|
|
274
|
+
Examples of when to use this tool:
|
|
275
|
+
- "What is 15% of 847?" \u2192 calculate("0.15 * 847")
|
|
276
|
+
- "Calculate 2^32" \u2192 calculate("2^32")
|
|
277
|
+
- "What's 3,456 \xD7 7,891?" \u2192 calculate("3456 * 7891")
|
|
278
|
+
- "Square root of 7" \u2192 calculate("sqrt(7)")
|
|
279
|
+
- "sin(30 degrees)" \u2192 calculate("sin(30 deg)")
|
|
280
|
+
- "12! / (4! * 8!)" \u2192 calculate("12! / (4! * 8!)")
|
|
281
|
+
|
|
282
|
+
Examples of when NOT to use this tool:
|
|
283
|
+
- Rough estimates ("about how many people fit in a stadium")
|
|
284
|
+
- Conceptual math explanations ("explain what a derivative is")
|
|
285
|
+
- Symbolic algebra that doesn't evaluate to a number`,
|
|
286
|
+
inputSchema: z.object({
|
|
287
|
+
expression: z.string().describe("Mathematical expression to evaluate, e.g. '(245 * 389) + (12^3 / 7)'"),
|
|
288
|
+
precision: z.number().optional().describe("Significant digits for the result. Default: 14")
|
|
289
|
+
}),
|
|
290
|
+
handler: async (args) => {
|
|
291
|
+
const result = evaluateExpression(args.expression, args.precision);
|
|
292
|
+
if ("error" in result) {
|
|
293
|
+
const { hint, examples } = getErrorHint("calculate", result.error);
|
|
294
|
+
return {
|
|
295
|
+
content: [
|
|
296
|
+
{
|
|
297
|
+
type: "text",
|
|
298
|
+
text: JSON.stringify({
|
|
299
|
+
error: result.error,
|
|
300
|
+
expression: args.expression,
|
|
301
|
+
hint,
|
|
302
|
+
examples
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
isError: true
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: "text",
|
|
313
|
+
text: JSON.stringify({
|
|
314
|
+
result: result.result,
|
|
315
|
+
expression: args.expression,
|
|
316
|
+
...result.note && { note: result.note }
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// src/tools/convert.ts
|
|
325
|
+
import { z as z2 } from "zod/v4";
|
|
326
|
+
var convertTool = {
|
|
327
|
+
name: "convert",
|
|
328
|
+
description: `Converts between units of measurement deterministically. Supports length, weight, volume, temperature, area, speed, time, data (bytes/bits), and 100+ other units.
|
|
329
|
+
|
|
330
|
+
Use this tool whenever a user asks to convert between units. The value, source unit, and target unit must be specified separately.
|
|
331
|
+
|
|
332
|
+
Common aliases like "mph", "kph", "knots" are supported in addition to standard mathjs units.
|
|
333
|
+
|
|
334
|
+
Examples:
|
|
335
|
+
- "Convert 5 km to miles" \u2192 convert(5, "km", "miles")
|
|
336
|
+
- "100\xB0F in Celsius" \u2192 convert(100, "fahrenheit", "celsius")
|
|
337
|
+
- "1 lb in kg" \u2192 convert(1, "lb", "kg")
|
|
338
|
+
- "1024 bytes to kB" \u2192 convert(1024, "bytes", "kB")
|
|
339
|
+
- "60 mph to km/h" \u2192 convert(60, "mph", "kph")`,
|
|
340
|
+
inputSchema: z2.object({
|
|
341
|
+
value: z2.number().describe("The numeric value to convert"),
|
|
342
|
+
from: z2.string().describe("Source unit, e.g. 'km', 'fahrenheit', 'lb'"),
|
|
343
|
+
to: z2.string().describe("Target unit, e.g. 'miles', 'celsius', 'kg'")
|
|
344
|
+
}),
|
|
345
|
+
handler: async (args) => {
|
|
346
|
+
const result = convertUnit(args.value, args.from, args.to);
|
|
347
|
+
if ("error" in result) {
|
|
348
|
+
const { hint, examples } = getErrorHint("convert", result.error);
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: JSON.stringify({
|
|
354
|
+
error: result.error,
|
|
355
|
+
value: args.value,
|
|
356
|
+
from: args.from,
|
|
357
|
+
to: args.to,
|
|
358
|
+
hint,
|
|
359
|
+
examples
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
isError: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: JSON.stringify({
|
|
371
|
+
result: result.result,
|
|
372
|
+
value: args.value,
|
|
373
|
+
from: args.from,
|
|
374
|
+
to: args.to,
|
|
375
|
+
...result.note && { note: result.note }
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/tools/statistics.ts
|
|
384
|
+
import { z as z3 } from "zod/v4";
|
|
385
|
+
var statisticsTool = {
|
|
386
|
+
name: "statistics",
|
|
387
|
+
description: `Computes statistical measures on a dataset deterministically. Use this tool when a user asks for mean, median, mode, standard deviation, variance, min, max, sum, or percentile calculations on a set of numbers.
|
|
388
|
+
|
|
389
|
+
Examples:
|
|
390
|
+
- "What's the average of these test scores?" \u2192 statistics("mean", [85, 92, 78, 95, 88])
|
|
391
|
+
- "Find the median household income" \u2192 statistics("median", [45000, 52000, 61000, 38000])
|
|
392
|
+
- "90th percentile of response times" \u2192 statistics("percentile", [120, 340, 200, 150, 180], 90)
|
|
393
|
+
- "Standard deviation of this sample" \u2192 statistics("std", [23, 45, 12, 67, 34])`,
|
|
394
|
+
inputSchema: z3.object({
|
|
395
|
+
operation: z3.enum(["mean", "median", "mode", "std", "variance", "min", "max", "sum", "percentile"]).describe("The statistical operation to perform"),
|
|
396
|
+
data: z3.array(z3.number()).describe("Array of numbers to compute the statistic on"),
|
|
397
|
+
percentile: z3.number().optional().describe('Percentile value (0-100), required if operation is "percentile"')
|
|
398
|
+
}),
|
|
399
|
+
handler: async (args) => {
|
|
400
|
+
const result = computeStatistic(args.operation, args.data, args.percentile);
|
|
401
|
+
if ("error" in result) {
|
|
402
|
+
const { hint, examples } = getErrorHint("statistics", result.error);
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: JSON.stringify({
|
|
408
|
+
error: result.error,
|
|
409
|
+
operation: args.operation,
|
|
410
|
+
hint,
|
|
411
|
+
examples
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
isError: true
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: "text",
|
|
422
|
+
text: JSON.stringify({
|
|
423
|
+
result: result.result,
|
|
424
|
+
operation: args.operation,
|
|
425
|
+
...result.note && { note: result.note }
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/index.ts
|
|
434
|
+
var require2 = createRequire(import.meta.url);
|
|
435
|
+
var { version } = require2("../package.json");
|
|
436
|
+
var server = new McpServer({
|
|
437
|
+
name: "euclid",
|
|
438
|
+
version
|
|
439
|
+
});
|
|
440
|
+
server.registerTool(
|
|
441
|
+
calculateTool.name,
|
|
442
|
+
{
|
|
443
|
+
description: calculateTool.description,
|
|
444
|
+
inputSchema: calculateTool.inputSchema
|
|
445
|
+
},
|
|
446
|
+
async (args) => calculateTool.handler(args)
|
|
447
|
+
);
|
|
448
|
+
server.registerTool(
|
|
449
|
+
convertTool.name,
|
|
450
|
+
{
|
|
451
|
+
description: convertTool.description,
|
|
452
|
+
inputSchema: convertTool.inputSchema
|
|
453
|
+
},
|
|
454
|
+
async (args) => convertTool.handler(args)
|
|
455
|
+
);
|
|
456
|
+
server.registerTool(
|
|
457
|
+
statisticsTool.name,
|
|
458
|
+
{
|
|
459
|
+
description: statisticsTool.description,
|
|
460
|
+
inputSchema: statisticsTool.inputSchema
|
|
461
|
+
},
|
|
462
|
+
async (args) => statisticsTool.handler(args)
|
|
463
|
+
);
|
|
464
|
+
var transport = new StdioServerTransport();
|
|
465
|
+
await server.connect(transport);
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
: << 'CMDBLOCK'
|
|
2
|
+
@echo off
|
|
3
|
+
REM Cross-platform polyglot wrapper for hook scripts.
|
|
4
|
+
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
|
|
5
|
+
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
|
|
6
|
+
|
|
7
|
+
if "%~1"=="" (
|
|
8
|
+
echo run-hook.cmd: missing script name >&2
|
|
9
|
+
exit /b 1
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
set "HOOK_DIR=%~dp0"
|
|
13
|
+
|
|
14
|
+
REM Try Git for Windows bash in standard locations
|
|
15
|
+
if exist "C:\Program Files\Git\bin\bash.exe" (
|
|
16
|
+
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
17
|
+
exit /b %ERRORLEVEL%
|
|
18
|
+
)
|
|
19
|
+
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
|
|
20
|
+
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
21
|
+
exit /b %ERRORLEVEL%
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin)
|
|
25
|
+
where bash >nul 2>nul
|
|
26
|
+
if %ERRORLEVEL% equ 0 (
|
|
27
|
+
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
28
|
+
exit /b %ERRORLEVEL%
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
REM No bash found - exit silently rather than error
|
|
32
|
+
REM (plugin still works, just without SessionStart context injection)
|
|
33
|
+
exit /b 0
|
|
34
|
+
CMDBLOCK
|
|
35
|
+
|
|
36
|
+
# Unix: run the named script directly
|
|
37
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
38
|
+
SCRIPT_NAME="$1"
|
|
39
|
+
shift
|
|
40
|
+
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook for Euclid plugin
|
|
3
|
+
# 1. Auto-registers MCP server if not already present
|
|
4
|
+
# 2. Injects context reminder that Euclid tools are available
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
9
|
+
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
10
|
+
|
|
11
|
+
# Check if euclid MCP server is already registered
|
|
12
|
+
if ! claude mcp list 2>/dev/null | grep -q "euclid"; then
|
|
13
|
+
claude mcp add euclid -- npx -y @euclid-tools/euclid 2>/dev/null || true
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# JSON-escape helper
|
|
17
|
+
escape_for_json() {
|
|
18
|
+
local s="$1"
|
|
19
|
+
s="${s//\\/\\\\}"
|
|
20
|
+
s="${s//\"/\\\"}"
|
|
21
|
+
s="${s//$'\n'/\\n}"
|
|
22
|
+
s="${s//$'\r'/\\r}"
|
|
23
|
+
s="${s//$'\t'/\\t}"
|
|
24
|
+
printf '%s' "$s"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
context="Euclid deterministic math tools are available via MCP. For ANY numerical computation, unit conversion, or statistical calculation, use the Euclid MCP tools (calculate, convert, statistics) instead of mental math. Never predict or estimate when a deterministic tool is available."
|
|
28
|
+
context_escaped=$(escape_for_json "$context")
|
|
29
|
+
|
|
30
|
+
# Output context injection
|
|
31
|
+
# Claude Code sets CLAUDE_PLUGIN_ROOT; other platforms do not
|
|
32
|
+
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
33
|
+
cat <<EOF
|
|
34
|
+
{
|
|
35
|
+
"hookSpecificOutput": {
|
|
36
|
+
"hookEventName": "SessionStart",
|
|
37
|
+
"additionalContext": "${context_escaped}"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
EOF
|
|
41
|
+
else
|
|
42
|
+
cat <<EOF
|
|
43
|
+
{
|
|
44
|
+
"additional_context": "${context_escaped}"
|
|
45
|
+
}
|
|
46
|
+
EOF
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@euclid-tools/euclid",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Deterministic math tools for LLMs — an MCP server and Claude Code plugin powered by mathjs",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/euclidtools/euclid.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"bin": {
|
|
12
|
+
"euclid-mcp": "dist/index.js",
|
|
13
|
+
"euclid": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"lint": "eslint src/",
|
|
21
|
+
"format": "prettier --write .",
|
|
22
|
+
"format:check": "prettier --check ."
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"calculator",
|
|
27
|
+
"math",
|
|
28
|
+
"deterministic",
|
|
29
|
+
"llm",
|
|
30
|
+
"mathjs"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
".claude-plugin",
|
|
35
|
+
"skills",
|
|
36
|
+
"hooks"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
44
|
+
"mathjs": "^15.1.1",
|
|
45
|
+
"zod": "^4.3.6"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^10.0.1",
|
|
49
|
+
"@types/node": "^25.4.0",
|
|
50
|
+
"eslint": "^10.0.3",
|
|
51
|
+
"prettier": "^3.8.1",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"typescript-eslint": "^8.57.0",
|
|
56
|
+
"vitest": "^4.0.18"
|
|
57
|
+
}
|
|
58
|
+
}
|