dhar_mcptools 0.1.0__py3-none-any.whl
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.
- dhar_mcptools/__init__.py +0 -0
- dhar_mcptools/main.py +11 -0
- dhar_mcptools/tools.py +351 -0
- dhar_mcptools-0.1.0.dist-info/METADATA +7 -0
- dhar_mcptools-0.1.0.dist-info/RECORD +8 -0
- dhar_mcptools-0.1.0.dist-info/WHEEL +5 -0
- dhar_mcptools-0.1.0.dist-info/entry_points.txt +2 -0
- dhar_mcptools-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
dhar_mcptools/main.py
ADDED
dhar_mcptools/tools.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Everyday MCP Toolkit
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
An MCP server exposing small, dependency-free utility tools useful for both
|
|
6
|
+
everyday tasks (password generation, unit conversion, date math) and
|
|
7
|
+
developer tasks (hashing, base64, UUIDs, case conversion, text stats).
|
|
8
|
+
|
|
9
|
+
Everything here uses only the Python standard library, so this server has
|
|
10
|
+
no runtime dependency beyond the `mcp` package itself — nothing to break,
|
|
11
|
+
no API keys, no network calls.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import hashlib
|
|
16
|
+
import re
|
|
17
|
+
import secrets
|
|
18
|
+
import string
|
|
19
|
+
import uuid
|
|
20
|
+
from datetime import date, timedelta
|
|
21
|
+
|
|
22
|
+
from fastmcp import FastMCP
|
|
23
|
+
|
|
24
|
+
mcp = FastMCP()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Unit conversion data (used by convert_units)
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
_LENGTH_ALIASES = {
|
|
32
|
+
"mm": "mm", "millimeter": "mm", "millimeters": "mm", "millimetre": "mm", "millimetres": "mm",
|
|
33
|
+
"cm": "cm", "centimeter": "cm", "centimeters": "cm", "centimetre": "cm", "centimetres": "cm",
|
|
34
|
+
"m": "m", "meter": "m", "meters": "m", "metre": "m", "metres": "m",
|
|
35
|
+
"km": "km", "kilometer": "km", "kilometers": "km", "kilometre": "km", "kilometres": "km",
|
|
36
|
+
"in": "in", "inch": "in", "inches": "in",
|
|
37
|
+
"ft": "ft", "foot": "ft", "feet": "ft",
|
|
38
|
+
"yd": "yd", "yard": "yd", "yards": "yd",
|
|
39
|
+
"mi": "mi", "mile": "mi", "miles": "mi",
|
|
40
|
+
}
|
|
41
|
+
_LENGTH_TO_METERS = {
|
|
42
|
+
"mm": 0.001, "cm": 0.01, "m": 1.0, "km": 1000.0,
|
|
43
|
+
"in": 0.0254, "ft": 0.3048, "yd": 0.9144, "mi": 1609.344,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_WEIGHT_ALIASES = {
|
|
47
|
+
"mg": "mg", "milligram": "mg", "milligrams": "mg",
|
|
48
|
+
"g": "g", "gram": "g", "grams": "g",
|
|
49
|
+
"kg": "kg", "kilogram": "kg", "kilograms": "kg",
|
|
50
|
+
"oz": "oz", "ounce": "oz", "ounces": "oz",
|
|
51
|
+
"lb": "lb", "lbs": "lb", "pound": "lb", "pounds": "lb",
|
|
52
|
+
"t": "t", "ton": "t", "tons": "t", "tonne": "t", "tonnes": "t",
|
|
53
|
+
}
|
|
54
|
+
_WEIGHT_TO_GRAMS = {
|
|
55
|
+
"mg": 0.001, "g": 1.0, "kg": 1000.0,
|
|
56
|
+
"oz": 28.349523125, "lb": 453.59237, "t": 1_000_000.0,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_VOLUME_ALIASES = {
|
|
60
|
+
"ml": "ml", "milliliter": "ml", "milliliters": "ml", "millilitre": "ml", "millilitres": "ml",
|
|
61
|
+
"l": "l", "liter": "l", "liters": "l", "litre": "l", "litres": "l",
|
|
62
|
+
"gal": "gal", "gallon": "gal", "gallons": "gal",
|
|
63
|
+
"qt": "qt", "quart": "qt", "quarts": "qt",
|
|
64
|
+
"pt": "pt", "pint": "pt", "pints": "pt",
|
|
65
|
+
"cup": "cup", "cups": "cup",
|
|
66
|
+
"floz": "floz", "fl oz": "floz", "fluidounce": "floz", "fluidounces": "floz",
|
|
67
|
+
"tbsp": "tbsp", "tablespoon": "tbsp", "tablespoons": "tbsp",
|
|
68
|
+
"tsp": "tsp", "teaspoon": "tsp", "teaspoons": "tsp",
|
|
69
|
+
}
|
|
70
|
+
_VOLUME_TO_LITERS = {
|
|
71
|
+
"ml": 0.001, "l": 1.0, "gal": 3.785411784, "qt": 0.946352946,
|
|
72
|
+
"pt": 0.473176473, "cup": 0.236588236, "floz": 0.0295735296,
|
|
73
|
+
"tbsp": 0.0147867648, "tsp": 0.0049289216,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_TEMPERATURE_ALIASES = {
|
|
77
|
+
"c": "c", "celsius": "c", "centigrade": "c",
|
|
78
|
+
"f": "f", "fahrenheit": "f",
|
|
79
|
+
"k": "k", "kelvin": "k",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_UNIT_TABLES = {
|
|
83
|
+
"length": (_LENGTH_ALIASES, _LENGTH_TO_METERS),
|
|
84
|
+
"weight": (_WEIGHT_ALIASES, _WEIGHT_TO_GRAMS),
|
|
85
|
+
"volume": (_VOLUME_ALIASES, _VOLUME_TO_LITERS),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_unit(unit: str):
|
|
90
|
+
"""Return (category, canonical_unit) for a unit string, or None if unrecognized."""
|
|
91
|
+
key = unit.strip().lower()
|
|
92
|
+
if key in _TEMPERATURE_ALIASES:
|
|
93
|
+
return "temperature", _TEMPERATURE_ALIASES[key]
|
|
94
|
+
for category, (aliases, _factors) in _UNIT_TABLES.items():
|
|
95
|
+
if key in aliases:
|
|
96
|
+
return category, aliases[key]
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
|
|
101
|
+
if from_unit == "c":
|
|
102
|
+
celsius = value
|
|
103
|
+
elif from_unit == "f":
|
|
104
|
+
celsius = (value - 32) * 5 / 9
|
|
105
|
+
else: # "k"
|
|
106
|
+
celsius = value - 273.15
|
|
107
|
+
|
|
108
|
+
if to_unit == "c":
|
|
109
|
+
return celsius
|
|
110
|
+
if to_unit == "f":
|
|
111
|
+
return celsius * 9 / 5 + 32
|
|
112
|
+
return celsius + 273.15 # "k"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
def convert_units(value: float, from_unit: str, to_unit: str) -> str:
|
|
117
|
+
"""Convert a numeric value between common units of length, weight, temperature, or volume.
|
|
118
|
+
|
|
119
|
+
Examples: km<->mi, kg<->lb, celsius<->fahrenheit, liter<->gallon.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
value: The numeric value to convert.
|
|
123
|
+
from_unit: Unit to convert from, e.g. "km", "lb", "celsius", "gallon".
|
|
124
|
+
to_unit: Unit to convert to, e.g. "mi", "kg", "fahrenheit", "liter".
|
|
125
|
+
"""
|
|
126
|
+
from_resolved = _resolve_unit(from_unit)
|
|
127
|
+
to_resolved = _resolve_unit(to_unit)
|
|
128
|
+
|
|
129
|
+
if from_resolved is None:
|
|
130
|
+
return f"Error: unrecognized unit '{from_unit}'."
|
|
131
|
+
if to_resolved is None:
|
|
132
|
+
return f"Error: unrecognized unit '{to_unit}'."
|
|
133
|
+
|
|
134
|
+
from_category, from_key = from_resolved
|
|
135
|
+
to_category, to_key = to_resolved
|
|
136
|
+
|
|
137
|
+
if from_category != to_category:
|
|
138
|
+
return (
|
|
139
|
+
f"Error: '{from_unit}' is a {from_category} unit but "
|
|
140
|
+
f"'{to_unit}' is a {to_category} unit — can't convert between them."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if from_category == "temperature":
|
|
144
|
+
result = _convert_temperature(value, from_key, to_key)
|
|
145
|
+
else:
|
|
146
|
+
_aliases, to_base = _UNIT_TABLES[from_category]
|
|
147
|
+
result = value * to_base[from_key] / to_base[to_key]
|
|
148
|
+
|
|
149
|
+
return f"{value} {from_unit} = {round(result, 6)} {to_unit}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
def generate_password(length: int = 16, use_symbols: bool = True, count: int = 1) -> str:
|
|
154
|
+
"""Generate one or more cryptographically secure random passwords.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
length: Length of each password, between 4 and 128 (default 16).
|
|
158
|
+
use_symbols: Whether to include symbols like !@#$%^&* (default True).
|
|
159
|
+
count: How many passwords to generate, between 1 and 20 (default 1).
|
|
160
|
+
"""
|
|
161
|
+
if not (4 <= length <= 128):
|
|
162
|
+
return "Error: length must be between 4 and 128."
|
|
163
|
+
if not (1 <= count <= 20):
|
|
164
|
+
return "Error: count must be between 1 and 20."
|
|
165
|
+
|
|
166
|
+
rng = secrets.SystemRandom()
|
|
167
|
+
pools = [string.ascii_lowercase, string.ascii_uppercase, string.digits]
|
|
168
|
+
if use_symbols:
|
|
169
|
+
pools.append("!@#$%^&*()-_=+[]{}")
|
|
170
|
+
all_chars = "".join(pools)
|
|
171
|
+
|
|
172
|
+
passwords = []
|
|
173
|
+
for _ in range(count):
|
|
174
|
+
# Guarantee at least one character from each selected pool, then fill
|
|
175
|
+
# the rest randomly, then shuffle so the guaranteed characters aren't
|
|
176
|
+
# always in the same positions.
|
|
177
|
+
chars = [rng.choice(pool) for pool in pools]
|
|
178
|
+
chars += [rng.choice(all_chars) for _ in range(length - len(pools))]
|
|
179
|
+
rng.shuffle(chars)
|
|
180
|
+
passwords.append("".join(chars))
|
|
181
|
+
|
|
182
|
+
return "\n".join(passwords)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@mcp.tool()
|
|
186
|
+
def date_calculator(operation: str, date1: str, date2: str = "", days: int = 0) -> str:
|
|
187
|
+
"""Calculate the difference between two dates, or add/subtract days from a date.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
operation: One of "difference", "add", or "subtract".
|
|
191
|
+
date1: The starting date, in YYYY-MM-DD format.
|
|
192
|
+
date2: The second date, in YYYY-MM-DD format. Required for "difference".
|
|
193
|
+
days: Number of days to add or subtract. Required for "add" or "subtract".
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
d1 = date.fromisoformat(date1.strip())
|
|
197
|
+
except ValueError:
|
|
198
|
+
return f"Error: '{date1}' is not a valid date. Use YYYY-MM-DD format."
|
|
199
|
+
|
|
200
|
+
op = operation.strip().lower()
|
|
201
|
+
|
|
202
|
+
if op == "difference":
|
|
203
|
+
if not date2:
|
|
204
|
+
return "Error: 'difference' requires date2 (YYYY-MM-DD)."
|
|
205
|
+
try:
|
|
206
|
+
d2 = date.fromisoformat(date2.strip())
|
|
207
|
+
except ValueError:
|
|
208
|
+
return f"Error: '{date2}' is not a valid date. Use YYYY-MM-DD format."
|
|
209
|
+
delta = abs((d2 - d1).days)
|
|
210
|
+
return f"{delta} day(s) between {date1} and {date2}."
|
|
211
|
+
|
|
212
|
+
if op in ("add", "subtract"):
|
|
213
|
+
offset = timedelta(days=days if op == "add" else -days)
|
|
214
|
+
result = (d1 + offset).isoformat()
|
|
215
|
+
sign = "plus" if op == "add" else "minus"
|
|
216
|
+
return f"{date1} {sign} {days} day(s) = {result}."
|
|
217
|
+
|
|
218
|
+
return "Error: operation must be 'difference', 'add', or 'subtract'."
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@mcp.tool()
|
|
222
|
+
def hash_text(text: str, algorithm: str = "sha256") -> str:
|
|
223
|
+
"""Generate a hash digest of the given text.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
text: The text to hash.
|
|
227
|
+
algorithm: One of "md5", "sha1", "sha256", "sha512" (default "sha256").
|
|
228
|
+
"""
|
|
229
|
+
algo = algorithm.strip().lower()
|
|
230
|
+
valid = {"md5", "sha1", "sha256", "sha512"}
|
|
231
|
+
if algo not in valid:
|
|
232
|
+
return f"Error: algorithm must be one of {sorted(valid)}."
|
|
233
|
+
digest = hashlib.new(algo, text.encode("utf-8")).hexdigest()
|
|
234
|
+
return f"{algo}: {digest}"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@mcp.tool()
|
|
238
|
+
def base64_convert(text: str, operation: str = "encode") -> str:
|
|
239
|
+
"""Encode text to Base64, or decode a Base64 string back to text.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
text: Plain text to encode, or a Base64 string to decode.
|
|
243
|
+
operation: "encode" or "decode" (default "encode").
|
|
244
|
+
"""
|
|
245
|
+
op = operation.strip().lower()
|
|
246
|
+
if op == "encode":
|
|
247
|
+
return base64.b64encode(text.encode("utf-8")).decode("ascii")
|
|
248
|
+
if op == "decode":
|
|
249
|
+
try:
|
|
250
|
+
decoded = base64.b64decode(text, validate=True)
|
|
251
|
+
except Exception:
|
|
252
|
+
return "Error: input is not valid Base64."
|
|
253
|
+
try:
|
|
254
|
+
return decoded.decode("utf-8")
|
|
255
|
+
except UnicodeDecodeError:
|
|
256
|
+
return f"Decoded bytes are not valid UTF-8 text: {decoded!r}"
|
|
257
|
+
return "Error: operation must be 'encode' or 'decode'."
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@mcp.tool()
|
|
261
|
+
def generate_uuid(count: int = 1, uppercase: bool = False) -> str:
|
|
262
|
+
"""Generate one or more random UUIDs (version 4).
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
count: How many UUIDs to generate, between 1 and 50 (default 1).
|
|
266
|
+
uppercase: Whether to return them uppercase (default False).
|
|
267
|
+
"""
|
|
268
|
+
if not (1 <= count <= 50):
|
|
269
|
+
return "Error: count must be between 1 and 50."
|
|
270
|
+
ids = [str(uuid.uuid4()) for _ in range(count)]
|
|
271
|
+
if uppercase:
|
|
272
|
+
ids = [i.upper() for i in ids]
|
|
273
|
+
return "\n".join(ids)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _split_words(text: str):
|
|
277
|
+
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", text)
|
|
278
|
+
spaced = re.sub(r"[_\-]+", " ", spaced)
|
|
279
|
+
return [w for w in spaced.split(" ") if w]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@mcp.tool()
|
|
283
|
+
def convert_case(text: str, target_case: str) -> str:
|
|
284
|
+
"""Convert text between naming styles: snake_case, camelCase, PascalCase,
|
|
285
|
+
kebab-case, CONSTANT_CASE, Title Case, or Sentence case.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
text: The text to convert (any of the above styles, or plain words).
|
|
289
|
+
target_case: One of "snake", "camel", "pascal", "kebab", "constant", "title", "sentence".
|
|
290
|
+
"""
|
|
291
|
+
words = _split_words(text)
|
|
292
|
+
if not words:
|
|
293
|
+
return "Error: no words found in input."
|
|
294
|
+
|
|
295
|
+
style = target_case.strip().lower()
|
|
296
|
+
if style == "snake":
|
|
297
|
+
return "_".join(w.lower() for w in words)
|
|
298
|
+
if style == "camel":
|
|
299
|
+
return words[0].lower() + "".join(w.capitalize() for w in words[1:])
|
|
300
|
+
if style == "pascal":
|
|
301
|
+
return "".join(w.capitalize() for w in words)
|
|
302
|
+
if style == "kebab":
|
|
303
|
+
return "-".join(w.lower() for w in words)
|
|
304
|
+
if style == "constant":
|
|
305
|
+
return "_".join(w.upper() for w in words)
|
|
306
|
+
if style == "title":
|
|
307
|
+
return " ".join(w.capitalize() for w in words)
|
|
308
|
+
if style == "sentence":
|
|
309
|
+
sentence = " ".join(w.lower() for w in words)
|
|
310
|
+
return sentence[0].upper() + sentence[1:]
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
"Error: target_case must be one of: snake, camel, pascal, kebab, "
|
|
314
|
+
"constant, title, sentence."
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@mcp.tool()
|
|
319
|
+
def text_stats(text: str) -> str:
|
|
320
|
+
"""Get statistics about a piece of text: character, word, sentence, and
|
|
321
|
+
paragraph counts, plus an estimated reading time.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
text: The text to analyze.
|
|
325
|
+
"""
|
|
326
|
+
char_count = len(text)
|
|
327
|
+
char_count_no_spaces = len(re.sub(r"\s", "", text))
|
|
328
|
+
words = text.split()
|
|
329
|
+
word_count = len(words)
|
|
330
|
+
sentence_count = len([s for s in re.split(r"[.!?]+", text) if s.strip()])
|
|
331
|
+
paragraphs = [p for p in re.split(r"\n\s*\n", text) if p.strip()]
|
|
332
|
+
paragraph_count = len(paragraphs) if paragraphs else (1 if text.strip() else 0)
|
|
333
|
+
reading_minutes = word_count / 200
|
|
334
|
+
reading_time = "< 1 min" if reading_minutes < 1 else f"~{round(reading_minutes)} min"
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
f"Characters: {char_count} ({char_count_no_spaces} without whitespace)\n"
|
|
338
|
+
f"Words: {word_count}\n"
|
|
339
|
+
f"Sentences: {sentence_count}\n"
|
|
340
|
+
f"Paragraphs: {paragraph_count}\n"
|
|
341
|
+
f"Estimated reading time: {reading_time}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def main() -> None:
|
|
346
|
+
"""Entry point for the `everyday-mcp-toolkit` console script."""
|
|
347
|
+
mcp.run()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
main()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dhar_mcptools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
dhar_mcptools/main.py,sha256=Kd-N7l0s9yxdH0RHZB7Ds62XfSE9ZAgtu7pFtBBqTNQ,158
|
|
3
|
+
dhar_mcptools/tools.py,sha256=AltMFQy6tKMq34TcVeBe9v9WZhNHx-J1nFUxzED3Bnc,12701
|
|
4
|
+
dhar_mcptools-0.1.0.dist-info/METADATA,sha256=6ikeQZ3P_fy78U9bwlFhHwt08FYqEo1UgVDiwqger1A,188
|
|
5
|
+
dhar_mcptools-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
dhar_mcptools-0.1.0.dist-info/entry_points.txt,sha256=LD29FLajPNUeFviiscOb_kkntlvUgXa9GbbduxakrhY,58
|
|
7
|
+
dhar_mcptools-0.1.0.dist-info/top_level.txt,sha256=XSEJ2yEUtrt3tbzyn30YJ6rdtlkBw7InblGpjv0JPNg,14
|
|
8
|
+
dhar_mcptools-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dhar_mcptools
|