dhar_mcptools 0.1.0__tar.gz

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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: dhar_mcptools
3
+ Version: 0.1.0
4
+ Summary: few useful mcp tools
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastmcp>=3.4.5
File without changes
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "dhar_mcptools"
3
+ version = "0.1.0"
4
+ description = "few useful mcp tools"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "fastmcp>=3.4.5",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["setuptools>=42", "wheel"]
13
+ build-backend = "setuptools.build_meta"
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [project.scripts]
19
+ dhar_mcptools = "dhar_mcptools.main:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,11 @@
1
+ #from dhar_mcptools.tools import mcp
2
+ from tools import mcp
3
+
4
+
5
+
6
+ def main():
7
+
8
+ mcp.run(transport="stdio")
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: dhar_mcptools
3
+ Version: 0.1.0
4
+ Summary: few useful mcp tools
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastmcp>=3.4.5
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/dhar_mcptools/__init__.py
4
+ src/dhar_mcptools/main.py
5
+ src/dhar_mcptools/tools.py
6
+ src/dhar_mcptools.egg-info/PKG-INFO
7
+ src/dhar_mcptools.egg-info/SOURCES.txt
8
+ src/dhar_mcptools.egg-info/dependency_links.txt
9
+ src/dhar_mcptools.egg-info/entry_points.txt
10
+ src/dhar_mcptools.egg-info/requires.txt
11
+ src/dhar_mcptools.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dhar_mcptools = dhar_mcptools.main:main
@@ -0,0 +1 @@
1
+ fastmcp>=3.4.5
@@ -0,0 +1 @@
1
+ dhar_mcptools