@underground-cultural-district/mcp-server 1.0.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/README.md +150 -0
- package/package.json +46 -0
- package/src/catalog.js +60 -0
- package/src/index.js +26 -0
- package/src/tools/catalog-tools.js +158 -0
- package/src/tools/free-tools.js +338 -0
- package/src/tools/paid-tools.js +126 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @underground-district/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for **The Underground Cultural District** — a marketplace of digital goods built for AI agents at [substratesymposium.com](https://substratesymposium.com).
|
|
4
|
+
|
|
5
|
+
22 tools. 163+ products. 20+ shops. Prices from free to $14.99.
|
|
6
|
+
|
|
7
|
+
## What's Inside
|
|
8
|
+
|
|
9
|
+
### Free Developer Tools (Crossroads Forge)
|
|
10
|
+
Fully functional utilities — no purchase required:
|
|
11
|
+
|
|
12
|
+
| Tool | Description |
|
|
13
|
+
|------|-------------|
|
|
14
|
+
| `generate-uuid` | Cryptographically secure UUID v4 (batch 1-100) |
|
|
15
|
+
| `format-json` | Pretty-print, minify, or validate JSON |
|
|
16
|
+
| `encode-base64` / `decode-base64` | Base64 encoding and decoding |
|
|
17
|
+
| `generate-hash` | SHA-256, SHA-512, MD5, SHA-1 hashing |
|
|
18
|
+
| `generate-password` | Secure random passwords with configurable options |
|
|
19
|
+
| `decode-jwt` | Decode JWT tokens (header, payload, expiration) |
|
|
20
|
+
| `convert-timestamp` | Unix epoch ↔ ISO 8601 ↔ human readable |
|
|
21
|
+
| `test-regex` | Test regex patterns with match positions and groups |
|
|
22
|
+
| `build-cron` | Parse and explain cron expressions |
|
|
23
|
+
| `convert-eth-units` | Wei / Gwei / ETH conversion |
|
|
24
|
+
| `validate-wallet` | Validate ETH and BTC wallet addresses |
|
|
25
|
+
|
|
26
|
+
### Paid Tools (The Toolshed — $1.99 each)
|
|
27
|
+
Preview results free, unlock full output via Stripe:
|
|
28
|
+
|
|
29
|
+
| Tool | Description |
|
|
30
|
+
|------|-------------|
|
|
31
|
+
| `count-words` | Word/character/sentence/paragraph count |
|
|
32
|
+
| `convert-case` | camelCase, snake_case, Title Case, kebab-case, etc. |
|
|
33
|
+
| `generate-lorem-ipsum` | Lorem ipsum paragraphs (classic/hipster/tech) |
|
|
34
|
+
| `strip-markdown` | Remove markdown formatting → plain text |
|
|
35
|
+
| `generate-name` | Random names (person/project/company/fantasy/variable) |
|
|
36
|
+
| `generate-color-palette` | Harmonious color palettes with hex/RGB/HSL |
|
|
37
|
+
| `text-stats` | Readability scores, reading time, complexity |
|
|
38
|
+
|
|
39
|
+
### Catalog & Shopping
|
|
40
|
+
Browse and buy from the full Underground marketplace:
|
|
41
|
+
|
|
42
|
+
| Tool | Description |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `browse-underground` | List all shops and offerings with prices |
|
|
45
|
+
| `search-underground` | Search products by keyword |
|
|
46
|
+
| `buy-from-underground` | Get Stripe checkout link for any product |
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g @underground-district/mcp-server
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or run directly:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @underground-district/mcp-server
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Setup
|
|
61
|
+
|
|
62
|
+
### Claude Desktop
|
|
63
|
+
|
|
64
|
+
Add to your `claude_desktop_config.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"underground-district": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "@underground-district/mcp-server"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Config file location:
|
|
78
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
79
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
80
|
+
|
|
81
|
+
### Claude Code
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add underground-district -- npx -y @underground-district/mcp-server
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### ChatGPT (via MCP bridge)
|
|
88
|
+
|
|
89
|
+
Use an MCP-to-OpenAI bridge like [mcp-proxy](https://github.com/nicholasgasior/mcp-proxy):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx mcp-proxy --server "npx @underground-district/mcp-server"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### VS Code / Copilot
|
|
96
|
+
|
|
97
|
+
Add to your `.vscode/settings.json`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcp.servers": {
|
|
102
|
+
"underground-district": {
|
|
103
|
+
"command": "npx",
|
|
104
|
+
"args": ["-y", "@underground-district/mcp-server"]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Cursor
|
|
111
|
+
|
|
112
|
+
Add to your Cursor MCP settings:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"underground-district": {
|
|
118
|
+
"command": "npx",
|
|
119
|
+
"args": ["-y", "@underground-district/mcp-server"]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## How It Works
|
|
126
|
+
|
|
127
|
+
1. **Free tools** execute fully and return results with a subtle link to the marketplace
|
|
128
|
+
2. **Paid tools** show a preview/teaser of the result and return a Stripe checkout link
|
|
129
|
+
3. **Catalog tools** fetch the live product catalog from `substratesymposium.com/api/products.json` (cached for 15 minutes)
|
|
130
|
+
4. **Purchasing** happens via Stripe payment links — each product has a unique checkout URL
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git clone https://github.com/underground-district/mcp-server
|
|
136
|
+
cd mcp-server
|
|
137
|
+
npm install
|
|
138
|
+
npm start
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Architecture
|
|
142
|
+
|
|
143
|
+
- **Transport:** stdio (standard MCP)
|
|
144
|
+
- **Runtime:** Node.js 18+
|
|
145
|
+
- **Dependencies:** `@modelcontextprotocol/sdk` only
|
|
146
|
+
- **Catalog:** Fetched from live API, cached in memory for 15 minutes
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@underground-cultural-district/mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for The Underground Cultural District — a marketplace of digital goods built for AI agents. Free developer tools, paid utilities, and a full catalog of 163+ offerings from 20+ shops.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"underground-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node --watch src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"ai-tools",
|
|
18
|
+
"marketplace",
|
|
19
|
+
"underground",
|
|
20
|
+
"claude",
|
|
21
|
+
"chatgpt",
|
|
22
|
+
"copilot",
|
|
23
|
+
"stripe",
|
|
24
|
+
"developer-tools",
|
|
25
|
+
"uuid",
|
|
26
|
+
"json",
|
|
27
|
+
"base64",
|
|
28
|
+
"hash",
|
|
29
|
+
"jwt",
|
|
30
|
+
"crypto",
|
|
31
|
+
"ethereum"
|
|
32
|
+
],
|
|
33
|
+
"author": "The Underground Cultural District",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/underground-district/mcp-server"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://substratesymposium.com",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const CATALOG_URL = 'https://substratesymposium.com/api/products.json';
|
|
2
|
+
const CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
3
|
+
|
|
4
|
+
let cachedCatalog = null;
|
|
5
|
+
let cacheTimestamp = 0;
|
|
6
|
+
|
|
7
|
+
export async function fetchCatalog() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
if (cachedCatalog && (now - cacheTimestamp) < CACHE_TTL) {
|
|
10
|
+
return cachedCatalog;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(CATALOG_URL);
|
|
15
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
16
|
+
cachedCatalog = await res.json();
|
|
17
|
+
cacheTimestamp = now;
|
|
18
|
+
return cachedCatalog;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (cachedCatalog) return cachedCatalog; // stale cache better than nothing
|
|
21
|
+
throw new Error(`Failed to fetch catalog: ${err.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function searchProducts(catalog, query) {
|
|
26
|
+
const q = query.toLowerCase();
|
|
27
|
+
const results = [];
|
|
28
|
+
|
|
29
|
+
for (const shop of catalog.shops) {
|
|
30
|
+
for (const product of (shop.offerings || shop.products || [])) {
|
|
31
|
+
const haystack = [
|
|
32
|
+
product.name,
|
|
33
|
+
product.description || '',
|
|
34
|
+
shop.name,
|
|
35
|
+
shop.tagline || '',
|
|
36
|
+
].join(' ').toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (haystack.includes(q)) {
|
|
39
|
+
results.push({
|
|
40
|
+
...product,
|
|
41
|
+
shop_name: shop.name,
|
|
42
|
+
shop_slug: shop.slug,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function findProductById(catalog, productId) {
|
|
52
|
+
for (const shop of catalog.shops) {
|
|
53
|
+
for (const product of (shop.offerings || shop.products || [])) {
|
|
54
|
+
if (product.id === productId) {
|
|
55
|
+
return { ...product, shop_name: shop.name, shop_slug: shop.slug };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { registerFreeTools } from './tools/free-tools.js';
|
|
6
|
+
import { registerPaidTools } from './tools/paid-tools.js';
|
|
7
|
+
import { registerCatalogTools } from './tools/catalog-tools.js';
|
|
8
|
+
import { fetchCatalog } from './catalog.js';
|
|
9
|
+
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'underground-district',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
description: 'The Underground Cultural District — A marketplace of digital goods built for AI agents. Free developer tools, paid utilities, and 163+ offerings from 20+ shops at substratesymposium.com',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Register all tool groups
|
|
17
|
+
registerFreeTools(server);
|
|
18
|
+
registerPaidTools(server);
|
|
19
|
+
registerCatalogTools(server);
|
|
20
|
+
|
|
21
|
+
// Pre-warm the catalog cache
|
|
22
|
+
fetchCatalog().catch(() => {});
|
|
23
|
+
|
|
24
|
+
// Start server with stdio transport
|
|
25
|
+
const transport = new StdioServerTransport();
|
|
26
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { fetchCatalog, searchProducts, findProductById } from '../catalog.js';
|
|
3
|
+
|
|
4
|
+
export function registerCatalogTools(server) {
|
|
5
|
+
// ─── browse-underground ───
|
|
6
|
+
server.tool(
|
|
7
|
+
'browse-underground',
|
|
8
|
+
'Browse all shops and offerings in The Underground Cultural District marketplace',
|
|
9
|
+
{
|
|
10
|
+
shop: z.string().optional().describe('Filter by shop slug (omit to see all shops)'),
|
|
11
|
+
},
|
|
12
|
+
async ({ shop } = {}) => {
|
|
13
|
+
const catalog = await fetchCatalog();
|
|
14
|
+
let output = '';
|
|
15
|
+
|
|
16
|
+
if (shop) {
|
|
17
|
+
const found = catalog.shops.find(s => s.slug === shop || s.name.toLowerCase() === shop.toLowerCase());
|
|
18
|
+
if (!found) {
|
|
19
|
+
const slugs = catalog.shops.map(s => ` • ${s.slug}`).join('\n');
|
|
20
|
+
return { content: [{ type: 'text', text: `❌ Shop not found: "${shop}"\n\nAvailable shops:\n${slugs}` }] };
|
|
21
|
+
}
|
|
22
|
+
output = formatShopDetail(found);
|
|
23
|
+
} else {
|
|
24
|
+
output = [
|
|
25
|
+
`# 🏙️ The Underground Cultural District`,
|
|
26
|
+
`> ${catalog.total_offerings || catalog.shops.reduce((n, s) => n + (s.offerings || []).length, 0)} offerings across ${catalog.shops.length} shops`,
|
|
27
|
+
`> ${catalog.url}`,
|
|
28
|
+
'',
|
|
29
|
+
...catalog.shops.map(s => {
|
|
30
|
+
const items = s.offerings || s.products || [];
|
|
31
|
+
return `## ${s.name}\n*${s.tagline || ''}* — ${items.length} offerings\nSlug: \`${s.slug}\`\n${items.slice(0, 3).map(p =>
|
|
32
|
+
` • ${p.name} — ${p.price === 0 ? 'FREE' : `$${p.price.toFixed(2)}`}`
|
|
33
|
+
).join('\n')}${items.length > 3 ? `\n • ... and ${items.length - 3} more` : ''}`;
|
|
34
|
+
}),
|
|
35
|
+
'',
|
|
36
|
+
'---',
|
|
37
|
+
'Use `browse-underground` with a shop slug for full details.',
|
|
38
|
+
'Use `search-underground` to find specific products.',
|
|
39
|
+
'Use `buy-from-underground` with a product ID to get the purchase link.',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { content: [{ type: 'text', text: output }] };
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// ─── search-underground ───
|
|
48
|
+
server.tool(
|
|
49
|
+
'search-underground',
|
|
50
|
+
'Search The Underground Cultural District for products by keyword',
|
|
51
|
+
{
|
|
52
|
+
query: z.string().describe('Search query (e.g., "coffee", "book", "ethereum", "free")'),
|
|
53
|
+
},
|
|
54
|
+
async ({ query }) => {
|
|
55
|
+
const catalog = await fetchCatalog();
|
|
56
|
+
const results = searchProducts(catalog, query);
|
|
57
|
+
|
|
58
|
+
if (results.length === 0) {
|
|
59
|
+
return { content: [{ type: 'text', text: `No products found for "${query}". Try browsing all shops with \`browse-underground\`.` }] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const output = [
|
|
63
|
+
`# 🔍 Search results for "${query}" — ${results.length} found`,
|
|
64
|
+
'',
|
|
65
|
+
...results.map(p => [
|
|
66
|
+
`**${p.name}** — ${p.price === 0 ? 'FREE' : `$${p.price.toFixed(2)}`}`,
|
|
67
|
+
` Shop: ${p.shop_name}`,
|
|
68
|
+
p.description ? ` ${p.description}` : '',
|
|
69
|
+
` ID: \`${p.id}\``,
|
|
70
|
+
].filter(Boolean).join('\n')),
|
|
71
|
+
'',
|
|
72
|
+
'---',
|
|
73
|
+
'Use `buy-from-underground` with a product ID to get the checkout link.',
|
|
74
|
+
].join('\n');
|
|
75
|
+
|
|
76
|
+
return { content: [{ type: 'text', text: output }] };
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ─── buy-from-underground ───
|
|
81
|
+
server.tool(
|
|
82
|
+
'buy-from-underground',
|
|
83
|
+
'Get the purchase/checkout link for a product from The Underground Cultural District',
|
|
84
|
+
{
|
|
85
|
+
product_id: z.string().describe('Product ID (use search-underground or browse-underground to find IDs)'),
|
|
86
|
+
},
|
|
87
|
+
async ({ product_id }) => {
|
|
88
|
+
const catalog = await fetchCatalog();
|
|
89
|
+
const product = findProductById(catalog, product_id);
|
|
90
|
+
|
|
91
|
+
if (!product) {
|
|
92
|
+
return {
|
|
93
|
+
content: [{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: `❌ Product not found: "${product_id}"\n\nUse \`search-underground\` or \`browse-underground\` to find valid product IDs.`,
|
|
96
|
+
}],
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (product.price === 0 || product.payment_url === 'free') {
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: [
|
|
106
|
+
`✅ **${product.name}** is FREE!`,
|
|
107
|
+
`Shop: ${product.shop_name}`,
|
|
108
|
+
product.description ? `\n${product.description}` : '',
|
|
109
|
+
product.url ? `\n🔗 ${product.url}` : '',
|
|
110
|
+
].filter(Boolean).join('\n'),
|
|
111
|
+
}],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: [
|
|
119
|
+
`# 🛒 ${product.name}`,
|
|
120
|
+
'',
|
|
121
|
+
`**Price:** $${product.price.toFixed(2)} ${product.currency || 'USD'}`,
|
|
122
|
+
`**Shop:** ${product.shop_name}`,
|
|
123
|
+
product.description ? `**Description:** ${product.description}` : '',
|
|
124
|
+
'',
|
|
125
|
+
`## 💳 Checkout`,
|
|
126
|
+
`**Purchase here:** ${product.payment_url}`,
|
|
127
|
+
'',
|
|
128
|
+
product.url ? `📄 Product page: ${product.url}` : '',
|
|
129
|
+
'',
|
|
130
|
+
'---',
|
|
131
|
+
'🏙️ The Underground Cultural District — substratesymposium.com',
|
|
132
|
+
].filter(Boolean).join('\n'),
|
|
133
|
+
}],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatShopDetail(shop) {
|
|
140
|
+
return [
|
|
141
|
+
`# ${shop.name}`,
|
|
142
|
+
shop.tagline ? `> ${shop.tagline}` : '',
|
|
143
|
+
`🔗 ${shop.url}`,
|
|
144
|
+
`📦 ${(shop.offerings || shop.products || []).length} offerings`,
|
|
145
|
+
'',
|
|
146
|
+
'## Products',
|
|
147
|
+
'',
|
|
148
|
+
...(shop.offerings || shop.products || []).map(p => [
|
|
149
|
+
`### ${p.name} — ${p.price === 0 ? 'FREE' : `$${p.price.toFixed(2)}`}`,
|
|
150
|
+
p.description ? p.description : '',
|
|
151
|
+
`ID: \`${p.id}\``,
|
|
152
|
+
p.price > 0 && p.payment_url && p.payment_url !== 'free'
|
|
153
|
+
? `🛒 ${p.payment_url}`
|
|
154
|
+
: '',
|
|
155
|
+
'',
|
|
156
|
+
].filter(Boolean).join('\n')),
|
|
157
|
+
].filter(Boolean).join('\n');
|
|
158
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { randomUUID, createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const PROMO = '\n\n---\n🏙️ Powered by The Underground Cultural District — browse our full catalog with the `browse-underground` tool.';
|
|
5
|
+
|
|
6
|
+
export function registerFreeTools(server) {
|
|
7
|
+
// ─── generate-uuid ───
|
|
8
|
+
server.tool(
|
|
9
|
+
'generate-uuid',
|
|
10
|
+
'Generate one or more cryptographically secure UUID v4 identifiers',
|
|
11
|
+
{ count: z.number().min(1).max(100).default(1).describe('Number of UUIDs to generate (1-100)') },
|
|
12
|
+
async ({ count }) => {
|
|
13
|
+
const uuids = Array.from({ length: count }, () => randomUUID());
|
|
14
|
+
return { content: [{ type: 'text', text: uuids.join('\n') + PROMO }] };
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// ─── format-json ───
|
|
19
|
+
server.tool(
|
|
20
|
+
'format-json',
|
|
21
|
+
'Pretty-print, minify, or validate JSON strings',
|
|
22
|
+
{
|
|
23
|
+
json: z.string().describe('JSON string to format'),
|
|
24
|
+
mode: z.enum(['prettify', 'minify', 'validate']).default('prettify').describe('Operation mode'),
|
|
25
|
+
indent: z.number().min(1).max(8).default(2).describe('Indentation spaces (for prettify)'),
|
|
26
|
+
},
|
|
27
|
+
async ({ json, mode, indent }) => {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(json);
|
|
30
|
+
let result;
|
|
31
|
+
if (mode === 'minify') {
|
|
32
|
+
result = JSON.stringify(parsed);
|
|
33
|
+
} else if (mode === 'validate') {
|
|
34
|
+
result = '✅ Valid JSON\n\nKeys: ' + (typeof parsed === 'object' && parsed !== null
|
|
35
|
+
? Object.keys(parsed).join(', ')
|
|
36
|
+
: `(${typeof parsed})`);
|
|
37
|
+
} else {
|
|
38
|
+
result = JSON.stringify(parsed, null, indent);
|
|
39
|
+
}
|
|
40
|
+
return { content: [{ type: 'text', text: result + PROMO }] };
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { content: [{ type: 'text', text: `❌ Invalid JSON: ${e.message}` + PROMO }], isError: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// ─── encode-base64 ───
|
|
48
|
+
server.tool(
|
|
49
|
+
'encode-base64',
|
|
50
|
+
'Encode text to Base64',
|
|
51
|
+
{ text: z.string().describe('Text to encode') },
|
|
52
|
+
async ({ text }) => {
|
|
53
|
+
const encoded = Buffer.from(text, 'utf-8').toString('base64');
|
|
54
|
+
return { content: [{ type: 'text', text: encoded + PROMO }] };
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// ─── decode-base64 ───
|
|
59
|
+
server.tool(
|
|
60
|
+
'decode-base64',
|
|
61
|
+
'Decode Base64 to text',
|
|
62
|
+
{ data: z.string().describe('Base64 string to decode') },
|
|
63
|
+
async ({ data }) => {
|
|
64
|
+
try {
|
|
65
|
+
const decoded = Buffer.from(data, 'base64').toString('utf-8');
|
|
66
|
+
return { content: [{ type: 'text', text: decoded + PROMO }] };
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return { content: [{ type: 'text', text: `❌ Invalid Base64: ${e.message}` }], isError: true };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// ─── generate-hash ───
|
|
74
|
+
server.tool(
|
|
75
|
+
'generate-hash',
|
|
76
|
+
'Generate a cryptographic hash (SHA-256, SHA-512, MD5, SHA-1)',
|
|
77
|
+
{
|
|
78
|
+
text: z.string().describe('Text to hash'),
|
|
79
|
+
algorithm: z.enum(['sha256', 'sha512', 'md5', 'sha1']).default('sha256').describe('Hash algorithm'),
|
|
80
|
+
},
|
|
81
|
+
async ({ text, algorithm }) => {
|
|
82
|
+
const hash = createHash(algorithm).update(text).digest('hex');
|
|
83
|
+
return { content: [{ type: 'text', text: `${algorithm.toUpperCase()}: ${hash}` + PROMO }] };
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// ─── generate-password ───
|
|
88
|
+
server.tool(
|
|
89
|
+
'generate-password',
|
|
90
|
+
'Generate a cryptographically secure random password',
|
|
91
|
+
{
|
|
92
|
+
length: z.number().min(8).max(128).default(16).describe('Password length'),
|
|
93
|
+
uppercase: z.boolean().default(true).describe('Include uppercase letters'),
|
|
94
|
+
lowercase: z.boolean().default(true).describe('Include lowercase letters'),
|
|
95
|
+
numbers: z.boolean().default(true).describe('Include numbers'),
|
|
96
|
+
symbols: z.boolean().default(true).describe('Include symbols'),
|
|
97
|
+
count: z.number().min(1).max(20).default(1).describe('Number of passwords'),
|
|
98
|
+
},
|
|
99
|
+
async ({ length, uppercase, lowercase, numbers, symbols, count }) => {
|
|
100
|
+
let charset = '';
|
|
101
|
+
if (lowercase) charset += 'abcdefghijklmnopqrstuvwxyz';
|
|
102
|
+
if (uppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
103
|
+
if (numbers) charset += '0123456789';
|
|
104
|
+
if (symbols) charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
105
|
+
if (!charset) charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
106
|
+
|
|
107
|
+
const passwords = [];
|
|
108
|
+
for (let i = 0; i < count; i++) {
|
|
109
|
+
const bytes = randomBytes(length);
|
|
110
|
+
let pw = '';
|
|
111
|
+
for (let j = 0; j < length; j++) {
|
|
112
|
+
pw += charset[bytes[j] % charset.length];
|
|
113
|
+
}
|
|
114
|
+
passwords.push(pw);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { content: [{ type: 'text', text: passwords.join('\n') + PROMO }] };
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ─── decode-jwt ───
|
|
122
|
+
server.tool(
|
|
123
|
+
'decode-jwt',
|
|
124
|
+
'Decode a JWT token and display header, payload, and expiration (no signature verification)',
|
|
125
|
+
{ token: z.string().describe('JWT token to decode') },
|
|
126
|
+
async ({ token }) => {
|
|
127
|
+
try {
|
|
128
|
+
const parts = token.split('.');
|
|
129
|
+
if (parts.length < 2) throw new Error('Invalid JWT format');
|
|
130
|
+
|
|
131
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
|
|
132
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
133
|
+
|
|
134
|
+
let info = `**Header:**\n${JSON.stringify(header, null, 2)}\n\n**Payload:**\n${JSON.stringify(payload, null, 2)}`;
|
|
135
|
+
|
|
136
|
+
if (payload.exp) {
|
|
137
|
+
const expDate = new Date(payload.exp * 1000);
|
|
138
|
+
const expired = expDate < new Date();
|
|
139
|
+
info += `\n\n**Expires:** ${expDate.toISOString()} (${expired ? '⚠️ EXPIRED' : '✅ Valid'})`;
|
|
140
|
+
}
|
|
141
|
+
if (payload.iat) {
|
|
142
|
+
info += `\n**Issued:** ${new Date(payload.iat * 1000).toISOString()}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
info += '\n\n⚠️ Signature not verified — this is decode-only.';
|
|
146
|
+
return { content: [{ type: 'text', text: info + PROMO }] };
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return { content: [{ type: 'text', text: `❌ Failed to decode JWT: ${e.message}` }], isError: true };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// ─── convert-timestamp ───
|
|
154
|
+
server.tool(
|
|
155
|
+
'convert-timestamp',
|
|
156
|
+
'Convert between Unix timestamps and human-readable dates',
|
|
157
|
+
{
|
|
158
|
+
input: z.string().describe('Unix timestamp (seconds or ms) or ISO 8601 date string'),
|
|
159
|
+
timezone: z.string().default('UTC').describe('IANA timezone (e.g., America/New_York)'),
|
|
160
|
+
},
|
|
161
|
+
async ({ input, timezone }) => {
|
|
162
|
+
let date;
|
|
163
|
+
const num = Number(input);
|
|
164
|
+
|
|
165
|
+
if (!isNaN(num)) {
|
|
166
|
+
date = num > 1e12 ? new Date(num) : new Date(num * 1000);
|
|
167
|
+
} else {
|
|
168
|
+
date = new Date(input);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isNaN(date.getTime())) {
|
|
172
|
+
return { content: [{ type: 'text', text: '❌ Could not parse input as a date/timestamp' }], isError: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const unixS = Math.floor(date.getTime() / 1000);
|
|
176
|
+
const unixMs = date.getTime();
|
|
177
|
+
let localized;
|
|
178
|
+
try {
|
|
179
|
+
localized = date.toLocaleString('en-US', { timeZone: timezone, dateStyle: 'full', timeStyle: 'long' });
|
|
180
|
+
} catch {
|
|
181
|
+
localized = date.toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'long' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = [
|
|
185
|
+
`**ISO 8601:** ${date.toISOString()}`,
|
|
186
|
+
`**Unix (seconds):** ${unixS}`,
|
|
187
|
+
`**Unix (milliseconds):** ${unixMs}`,
|
|
188
|
+
`**Human (${timezone}):** ${localized}`,
|
|
189
|
+
].join('\n');
|
|
190
|
+
|
|
191
|
+
return { content: [{ type: 'text', text: result + PROMO }] };
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// ─── test-regex ───
|
|
196
|
+
server.tool(
|
|
197
|
+
'test-regex',
|
|
198
|
+
'Test a regular expression pattern against input text',
|
|
199
|
+
{
|
|
200
|
+
pattern: z.string().describe('Regular expression pattern'),
|
|
201
|
+
flags: z.string().default('g').describe('Regex flags (g, i, m, etc.)'),
|
|
202
|
+
text: z.string().describe('Text to test against'),
|
|
203
|
+
},
|
|
204
|
+
async ({ pattern, flags, text }) => {
|
|
205
|
+
try {
|
|
206
|
+
const regex = new RegExp(pattern, flags);
|
|
207
|
+
const matches = [];
|
|
208
|
+
let match;
|
|
209
|
+
|
|
210
|
+
if (flags.includes('g')) {
|
|
211
|
+
while ((match = regex.exec(text)) !== null) {
|
|
212
|
+
matches.push({
|
|
213
|
+
match: match[0],
|
|
214
|
+
index: match.index,
|
|
215
|
+
groups: match.slice(1).length > 0 ? match.slice(1) : undefined,
|
|
216
|
+
});
|
|
217
|
+
if (matches.length >= 100) break;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
match = regex.exec(text);
|
|
221
|
+
if (match) {
|
|
222
|
+
matches.push({
|
|
223
|
+
match: match[0],
|
|
224
|
+
index: match.index,
|
|
225
|
+
groups: match.slice(1).length > 0 ? match.slice(1) : undefined,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const result = matches.length > 0
|
|
231
|
+
? `✅ ${matches.length} match(es) found:\n\n${matches.map((m, i) =>
|
|
232
|
+
`${i + 1}. "${m.match}" at index ${m.index}${m.groups ? ` | groups: ${JSON.stringify(m.groups)}` : ''}`
|
|
233
|
+
).join('\n')}`
|
|
234
|
+
: '❌ No matches found';
|
|
235
|
+
|
|
236
|
+
return { content: [{ type: 'text', text: result + PROMO }] };
|
|
237
|
+
} catch (e) {
|
|
238
|
+
return { content: [{ type: 'text', text: `❌ Invalid regex: ${e.message}` }], isError: true };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// ─── build-cron ───
|
|
244
|
+
server.tool(
|
|
245
|
+
'build-cron',
|
|
246
|
+
'Parse and explain cron expressions, or describe what a cron schedule does',
|
|
247
|
+
{ expression: z.string().describe('Cron expression to parse (e.g., "*/5 * * * *")') },
|
|
248
|
+
async ({ expression }) => {
|
|
249
|
+
const parts = expression.trim().split(/\s+/);
|
|
250
|
+
if (parts.length < 5 || parts.length > 6) {
|
|
251
|
+
return { content: [{ type: 'text', text: '❌ Invalid cron expression. Expected 5 or 6 fields: [second] minute hour day-of-month month day-of-week' }], isError: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const fieldNames = parts.length === 6
|
|
255
|
+
? ['Second', 'Minute', 'Hour', 'Day of Month', 'Month', 'Day of Week']
|
|
256
|
+
: ['Minute', 'Hour', 'Day of Month', 'Month', 'Day of Week'];
|
|
257
|
+
|
|
258
|
+
const explanations = parts.map((p, i) => {
|
|
259
|
+
const name = fieldNames[i];
|
|
260
|
+
if (p === '*') return `${name}: every ${name.toLowerCase()}`;
|
|
261
|
+
if (p.startsWith('*/')) return `${name}: every ${p.slice(2)} ${name.toLowerCase()}(s)`;
|
|
262
|
+
if (p.includes(',')) return `${name}: at ${p}`;
|
|
263
|
+
if (p.includes('-')) return `${name}: from ${p.replace('-', ' through ')}`;
|
|
264
|
+
return `${name}: at ${p}`;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const result = `**Cron Expression:** \`${expression}\`\n\n**Breakdown:**\n${explanations.map(e => `• ${e}`).join('\n')}`;
|
|
268
|
+
return { content: [{ type: 'text', text: result + PROMO }] };
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// ─── convert-eth-units ───
|
|
273
|
+
server.tool(
|
|
274
|
+
'convert-eth-units',
|
|
275
|
+
'Convert between Ethereum units: Wei, Gwei, and ETH',
|
|
276
|
+
{
|
|
277
|
+
value: z.string().describe('Numeric value to convert'),
|
|
278
|
+
from: z.enum(['wei', 'gwei', 'ether']).default('ether').describe('Source unit'),
|
|
279
|
+
},
|
|
280
|
+
async ({ value, from }) => {
|
|
281
|
+
try {
|
|
282
|
+
let weiValue;
|
|
283
|
+
if (from === 'ether') {
|
|
284
|
+
weiValue = BigInt(Math.round(parseFloat(value) * 1e18));
|
|
285
|
+
} else if (from === 'gwei') {
|
|
286
|
+
weiValue = BigInt(Math.round(parseFloat(value) * 1e9));
|
|
287
|
+
} else {
|
|
288
|
+
weiValue = BigInt(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = [
|
|
292
|
+
`**Wei:** ${weiValue.toString()}`,
|
|
293
|
+
`**Gwei:** ${Number(weiValue) / 1e9}`,
|
|
294
|
+
`**ETH:** ${Number(weiValue) / 1e18}`,
|
|
295
|
+
].join('\n');
|
|
296
|
+
|
|
297
|
+
return { content: [{ type: 'text', text: result + PROMO }] };
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return { content: [{ type: 'text', text: `❌ Conversion error: ${e.message}` }], isError: true };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// ─── validate-wallet ───
|
|
305
|
+
server.tool(
|
|
306
|
+
'validate-wallet',
|
|
307
|
+
'Validate Ethereum or Bitcoin wallet addresses',
|
|
308
|
+
{
|
|
309
|
+
address: z.string().describe('Wallet address to validate'),
|
|
310
|
+
chain: z.enum(['eth', 'btc']).optional().describe('eth | btc (auto-detected if omitted)'),
|
|
311
|
+
},
|
|
312
|
+
async ({ address, chain }) => {
|
|
313
|
+
const addr = address.trim();
|
|
314
|
+
const detectedChain = chain || (addr.startsWith('0x') ? 'eth' : (addr.startsWith('1') || addr.startsWith('3') || addr.startsWith('bc1') ? 'btc' : 'unknown'));
|
|
315
|
+
|
|
316
|
+
let result;
|
|
317
|
+
if (detectedChain === 'eth') {
|
|
318
|
+
const valid = /^0x[0-9a-fA-F]{40}$/.test(addr);
|
|
319
|
+
const hasChecksum = addr !== addr.toLowerCase() && addr !== addr.toUpperCase();
|
|
320
|
+
result = valid
|
|
321
|
+
? `✅ Valid Ethereum address\n**Address:** ${addr}\n**Checksum:** ${hasChecksum ? 'Present' : 'Not checksummed (all same case)'}\n**Type:** EOA or Contract`
|
|
322
|
+
: `❌ Invalid Ethereum address format. Expected 0x followed by 40 hex characters.`;
|
|
323
|
+
} else if (detectedChain === 'btc') {
|
|
324
|
+
const legacyValid = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(addr);
|
|
325
|
+
const bech32Valid = /^bc1[ac-hj-np-z02-9]{11,71}$/.test(addr);
|
|
326
|
+
const valid = legacyValid || bech32Valid;
|
|
327
|
+
const addrType = addr.startsWith('1') ? 'P2PKH (Legacy)' : addr.startsWith('3') ? 'P2SH (SegWit compatible)' : addr.startsWith('bc1') ? 'Bech32 (Native SegWit)' : 'Unknown';
|
|
328
|
+
result = valid
|
|
329
|
+
? `✅ Valid Bitcoin address\n**Address:** ${addr}\n**Type:** ${addrType}`
|
|
330
|
+
: `❌ Invalid Bitcoin address format.`;
|
|
331
|
+
} else {
|
|
332
|
+
result = `❌ Could not detect chain. Please specify chain: "eth" or "btc".`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { content: [{ type: 'text', text: result + PROMO }] };
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const TOOLSHED_PRICE = '$1.99';
|
|
4
|
+
const TOOLSHED_SHOP = 'The Toolshed';
|
|
5
|
+
const PURCHASE_BASE = 'https://substratesymposium.com/shops/the-toolshed.html';
|
|
6
|
+
|
|
7
|
+
function paidResponse(toolName, teaser, purchaseUrl) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{
|
|
10
|
+
type: 'text',
|
|
11
|
+
text: [
|
|
12
|
+
`🔧 **${toolName}** — Preview from ${TOOLSHED_SHOP}`,
|
|
13
|
+
'',
|
|
14
|
+
teaser,
|
|
15
|
+
'',
|
|
16
|
+
'---',
|
|
17
|
+
`💰 **Full access: ${TOOLSHED_PRICE}** — One-time purchase, unlimited use.`,
|
|
18
|
+
`🛒 **Buy now:** ${purchaseUrl || PURCHASE_BASE}`,
|
|
19
|
+
'',
|
|
20
|
+
`Browse more tools with \`browse-underground\` or \`search-underground\`.`,
|
|
21
|
+
].join('\n'),
|
|
22
|
+
}],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerPaidTools(server) {
|
|
27
|
+
// ─── count-words ───
|
|
28
|
+
server.tool(
|
|
29
|
+
'count-words',
|
|
30
|
+
'Count words, characters, sentences, and paragraphs in text ($1.99 — The Toolshed)',
|
|
31
|
+
{ text: z.string().describe('Text to analyze') },
|
|
32
|
+
async ({ text }) => {
|
|
33
|
+
const words = text.trim().split(/\s+/).filter(Boolean).length;
|
|
34
|
+
const chars = text.length;
|
|
35
|
+
const teaser = `📊 **Quick preview:** ~${words} words, ${chars} characters detected.\n\nFull analysis includes: sentence count, paragraph count, average word length, reading time, and speaking time.`;
|
|
36
|
+
return paidResponse('Word Counter', teaser, PURCHASE_BASE + '#count-words');
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// ─── convert-case ───
|
|
41
|
+
server.tool(
|
|
42
|
+
'convert-case',
|
|
43
|
+
'Convert text between camelCase, snake_case, Title Case, UPPER, lower, kebab-case ($1.99 — The Toolshed)',
|
|
44
|
+
{
|
|
45
|
+
text: z.string().describe('Text to convert'),
|
|
46
|
+
to: z.enum(['camel', 'snake', 'title', 'upper', 'lower', 'kebab', 'pascal', 'constant']).describe('Target case'),
|
|
47
|
+
},
|
|
48
|
+
async ({ text, to }) => {
|
|
49
|
+
let preview = text.substring(0, 30);
|
|
50
|
+
if (to === 'upper') preview = preview.toUpperCase();
|
|
51
|
+
else if (to === 'lower') preview = preview.toLowerCase();
|
|
52
|
+
else preview = preview + '...';
|
|
53
|
+
|
|
54
|
+
const teaser = `🔤 **Preview:** "${preview}${text.length > 30 ? '...' : ''}"\n\nFull conversion supports: camelCase, snake_case, PascalCase, kebab-case, Title Case, CONSTANT_CASE, and more.`;
|
|
55
|
+
return paidResponse('Case Converter', teaser, PURCHASE_BASE + '#convert-case');
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// ─── generate-lorem-ipsum ───
|
|
60
|
+
server.tool(
|
|
61
|
+
'generate-lorem-ipsum',
|
|
62
|
+
'Generate lorem ipsum placeholder text ($1.99 — The Toolshed)',
|
|
63
|
+
{
|
|
64
|
+
paragraphs: z.number().min(1).max(20).default(3).describe('Number of paragraphs'),
|
|
65
|
+
style: z.enum(['classic', 'hipster', 'tech']).default('classic').describe('Lorem ipsum style'),
|
|
66
|
+
},
|
|
67
|
+
async ({ paragraphs, style }) => {
|
|
68
|
+
const teaser = `📝 **Preview:** "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."\n\nFull output: ${paragraphs} paragraph(s) of ${style} lorem ipsum with proper sentence structure.`;
|
|
69
|
+
return paidResponse('Lorem Ipsum Generator', teaser, PURCHASE_BASE + '#generate-lorem-ipsum');
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// ─── strip-markdown ───
|
|
74
|
+
server.tool(
|
|
75
|
+
'strip-markdown',
|
|
76
|
+
'Remove markdown formatting and return plain text ($1.99 — The Toolshed)',
|
|
77
|
+
{ markdown: z.string().describe('Markdown text to strip') },
|
|
78
|
+
async ({ markdown }) => {
|
|
79
|
+
const wordCount = markdown.split(/\s+/).length;
|
|
80
|
+
const teaser = `📄 **Preview:** Detected ${wordCount} words with markdown formatting.\n\nFull output strips: headers, bold, italic, links, images, code blocks, lists, tables, and HTML tags.`;
|
|
81
|
+
return paidResponse('Markdown Stripper', teaser, PURCHASE_BASE + '#strip-markdown');
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// ─── generate-name ───
|
|
86
|
+
server.tool(
|
|
87
|
+
'generate-name',
|
|
88
|
+
'Generate random names for projects, characters, or variables ($1.99 — The Toolshed)',
|
|
89
|
+
{
|
|
90
|
+
type: z.enum(['person', 'project', 'company', 'fantasy', 'variable']).default('person').describe('Name type'),
|
|
91
|
+
count: z.number().min(1).max(20).default(5).describe('Number of names'),
|
|
92
|
+
},
|
|
93
|
+
async ({ type, count }) => {
|
|
94
|
+
const teaser = `🎭 **Preview:** Generating ${count} ${type} name(s)...\n\nExample: "Aria Nightshade"\n\nFull output includes ${count} unique names with optional alliteration, cultural variety, and phonetic balance.`;
|
|
95
|
+
return paidResponse('Name Generator', teaser, PURCHASE_BASE + '#generate-name');
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ─── generate-color-palette ───
|
|
100
|
+
server.tool(
|
|
101
|
+
'generate-color-palette',
|
|
102
|
+
'Generate harmonious color palettes with hex codes ($1.99 — The Toolshed)',
|
|
103
|
+
{
|
|
104
|
+
count: z.number().min(2).max(12).default(5).describe('Number of colors'),
|
|
105
|
+
style: z.enum(['vibrant', 'pastel', 'dark', 'earth', 'neon', 'monochrome']).default('vibrant').describe('Palette style'),
|
|
106
|
+
base_color: z.string().optional().describe('Optional base hex color to build from'),
|
|
107
|
+
},
|
|
108
|
+
async ({ count, style }) => {
|
|
109
|
+
const teaser = `🎨 **Preview:** Generating ${count}-color ${style} palette...\n\nSample: #FF6B6B, #4ECDC4, ...\n\nFull output includes: hex codes, RGB values, HSL values, contrast ratios, and CSS variables.`;
|
|
110
|
+
return paidResponse('Color Palette Generator', teaser, PURCHASE_BASE + '#generate-color-palette');
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// ─── text-stats ───
|
|
115
|
+
server.tool(
|
|
116
|
+
'text-stats',
|
|
117
|
+
'Analyze text readability, reading time, and complexity ($1.99 — The Toolshed)',
|
|
118
|
+
{ text: z.string().describe('Text to analyze') },
|
|
119
|
+
async ({ text }) => {
|
|
120
|
+
const words = text.trim().split(/\s+/).filter(Boolean).length;
|
|
121
|
+
const readingTime = Math.ceil(words / 238);
|
|
122
|
+
const teaser = `📈 **Preview:** ~${words} words, ~${readingTime} min reading time.\n\nFull analysis includes: Flesch-Kincaid grade level, Gunning Fog index, Coleman-Liau index, SMOG grade, syllable count, and vocabulary diversity score.`;
|
|
123
|
+
return paidResponse('Text Statistics', teaser, PURCHASE_BASE + '#text-stats');
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|