@sampuli/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/package.json +53 -0
- package/src/bin.js +10 -0
- package/src/server.js +54 -0
- package/src/tools.js +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @sampuli/mcp
|
|
2
|
+
|
|
3
|
+
**Generate format-true synthetic test data for 90 countries — right inside your AI assistant.**
|
|
4
|
+
|
|
5
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for [Sampuli](https://sampuli.site). Ask Claude, Cursor, Copilot or any MCP client to *"generate 50 German customers with valid IBANs"* and get data back inline — national IDs, tax numbers, IBANs, SWIFT/BIC, payment cards, phone numbers and full KYC records, each correct for its own country and passing real validators (IBAN mod-97, card Luhn, …).
|
|
6
|
+
|
|
7
|
+
Entirely synthetic: real formats, never real or registered data. Same engine as the [`@sampuli/data`](https://www.npmjs.com/package/@sampuli/data) npm package and [sampuli.site](https://sampuli.site).
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `sampuli_list_countries` | List all 90 countries and their codes |
|
|
14
|
+
| `sampuli_describe_country` | A country's fields and preset scenarios |
|
|
15
|
+
| `sampuli_generate` | Generate data — `spec`, `count`, `fields`, `seed`, `format` |
|
|
16
|
+
|
|
17
|
+
`spec` is `<country>.<selector>`, where the selector is a field key (`ke.phone`), `person` for a full record (`de.person`), or `preset:<key>` for a scenario (`ke.preset:kyc`).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
No install needed — `npx` runs it on demand.
|
|
22
|
+
|
|
23
|
+
### Claude Desktop / Claude Code
|
|
24
|
+
|
|
25
|
+
Add to your MCP config (`claude_desktop_config.json`, or `.mcp.json` in a project):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"sampuli": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "@sampuli/mcp"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Cursor
|
|
39
|
+
|
|
40
|
+
`~/.cursor/mcp.json` (or **Settings → MCP → Add**):
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"sampuli": { "command": "npx", "args": ["-y", "@sampuli/mcp"] }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Restart the client, then ask it to generate test data.
|
|
51
|
+
|
|
52
|
+
## Try it
|
|
53
|
+
|
|
54
|
+
> "List every country Sampuli supports."
|
|
55
|
+
> "Generate 100 Nigerian people with name, phone and BVN as CSV."
|
|
56
|
+
> "Give me one UK IBAN and one German test card number."
|
|
57
|
+
> "Generate a Kenyan KYC scenario."
|
|
58
|
+
|
|
59
|
+
## Prefer a URL or an install?
|
|
60
|
+
|
|
61
|
+
- **REST API** (no key): `curl https://sampuli.site/api/v1/ke/person?n=100`
|
|
62
|
+
- **npm**: `npm i @sampuli/data`
|
|
63
|
+
- **Browser**: [sampuli.site](https://sampuli.site)
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sampuli/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol server for Sampuli — generate format-true synthetic test data for 90 countries directly inside Claude, Cursor, Copilot and any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sampuli-mcp": "./src/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node src/bin.js",
|
|
19
|
+
"test": "vitest run"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"claude",
|
|
25
|
+
"cursor",
|
|
26
|
+
"copilot",
|
|
27
|
+
"test-data",
|
|
28
|
+
"synthetic-data",
|
|
29
|
+
"fake-data",
|
|
30
|
+
"iban",
|
|
31
|
+
"fintech",
|
|
32
|
+
"sampuli"
|
|
33
|
+
],
|
|
34
|
+
"author": "Sampuli",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"homepage": "https://sampuli.site",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/kevinbett/sampuli.git",
|
|
40
|
+
"directory": "packages/mcp"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/kevinbett/sampuli/issues"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
47
|
+
"@sampuli/data": "^0.3.0",
|
|
48
|
+
"zod": "^3.25.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"vitest": "^2.1.9"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/bin.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point: start the Sampuli MCP server over stdio.
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
4
|
+
import { createServer } from './server.js'
|
|
5
|
+
|
|
6
|
+
const server = createServer()
|
|
7
|
+
const transport = new StdioServerTransport()
|
|
8
|
+
await server.connect(transport)
|
|
9
|
+
// stderr is safe for logs; stdout is the MCP channel.
|
|
10
|
+
console.error('sampuli-mcp: ready (stdio)')
|
package/src/server.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Sampuli MCP server — exposes the synthetic-data engine as Model Context
|
|
2
|
+
// Protocol tools so any MCP client (Claude Desktop, Cursor, Copilot, …) can
|
|
3
|
+
// generate format-true test data inline. Transport is stdio (see bin.js).
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { listCountries, describeCountry, generateData } from './tools.js'
|
|
7
|
+
|
|
8
|
+
export function createServer() {
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'sampuli',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
server.registerTool(
|
|
15
|
+
'sampuli_list_countries',
|
|
16
|
+
{
|
|
17
|
+
title: 'List Sampuli countries',
|
|
18
|
+
description: 'List every country Sampuli can generate synthetic test data for (90 countries), with the ISO code used as a spec prefix.',
|
|
19
|
+
inputSchema: {},
|
|
20
|
+
},
|
|
21
|
+
async () => listCountries(),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
server.registerTool(
|
|
25
|
+
'sampuli_describe_country',
|
|
26
|
+
{
|
|
27
|
+
title: 'Describe a country',
|
|
28
|
+
description: "List a country's available fields (phone, national ID, tax number, IBAN, SWIFT/BIC, card, …) and preset scenarios.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
country: z.string().describe('ISO code, e.g. "ke", "de", "us", "ng".'),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
async args => describeCountry(args),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
server.registerTool(
|
|
37
|
+
'sampuli_generate',
|
|
38
|
+
{
|
|
39
|
+
title: 'Generate synthetic test data',
|
|
40
|
+
description:
|
|
41
|
+
'Generate format-true, entirely synthetic test data for a country. spec is "<country>.<selector>" where selector is a field key ("phone"), "person" (a full coherent record), or "preset:<key>" (a scenario). Values match each country\'s real formats and pass validators (IBAN mod-97, card Luhn, …) but are never real or registered.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
spec: z.string().describe('e.g. "ke.person", "de.iban", "ng.phone", "ke.preset:kyc".'),
|
|
44
|
+
count: z.number().int().min(1).max(10000).optional().describe('How many rows (omit for one).'),
|
|
45
|
+
fields: z.union([z.array(z.string()), z.literal('all')]).optional().describe('Which fields for a person record, or "all".'),
|
|
46
|
+
seed: z.string().optional().describe('Reproducible output — same seed, same data.'),
|
|
47
|
+
format: z.enum(['json', 'csv']).optional().describe('Output format (default json).'),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
async args => generateData(args),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return server
|
|
54
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Pure tool implementations for the Sampuli MCP server.
|
|
2
|
+
// Kept separate from the transport so they can be unit-tested directly.
|
|
3
|
+
// Each returns a Model Context Protocol tool result: { content: [...], isError? }.
|
|
4
|
+
import { generate, generateMany, listPacks, listFields, listPresets } from '@sampuli/data'
|
|
5
|
+
|
|
6
|
+
const text = t => ({ content: [{ type: 'text', text: t }] })
|
|
7
|
+
const fail = t => ({ content: [{ type: 'text', text: t }], isError: true })
|
|
8
|
+
|
|
9
|
+
const csvCell = v => { const s = v == null ? '' : String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s }
|
|
10
|
+
function toCsv(rows) {
|
|
11
|
+
const arr = Array.isArray(rows) ? rows : [rows]
|
|
12
|
+
if (!arr.length) return ''
|
|
13
|
+
if (typeof arr[0] !== 'object' || Array.isArray(arr[0])) return arr.join('\n')
|
|
14
|
+
const keys = Object.keys(arr[0])
|
|
15
|
+
return [keys.join(','), ...arr.map(r => keys.map(k => csvCell(r[k])).join(','))].join('\n')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** List every country pack, with its ISO code. */
|
|
19
|
+
export function listCountries() {
|
|
20
|
+
const codes = listPacks()
|
|
21
|
+
const lines = codes.map(c => `${c.toLowerCase()}`)
|
|
22
|
+
return text(`${codes.length} countries available (use the lower-case code as a spec prefix, e.g. "ke.person"):\n\n${lines.join(', ')}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Describe a country's fields and preset scenarios. */
|
|
26
|
+
export function describeCountry({ country }) {
|
|
27
|
+
const code = String(country || '').toLowerCase()
|
|
28
|
+
let fields, presets
|
|
29
|
+
try {
|
|
30
|
+
fields = listFields(code)
|
|
31
|
+
presets = listPresets(code)
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return fail(e.message)
|
|
34
|
+
}
|
|
35
|
+
const fieldLines = fields.map(f => ` ${f.key.padEnd(16)} ${f.label}`).join('\n')
|
|
36
|
+
const presetLines = presets.length
|
|
37
|
+
? '\n\nPreset scenarios (use spec "' + code + '.preset:<key>"):\n' + presets.map(p => ` ${p.key.padEnd(16)} ${p.name}`).join('\n')
|
|
38
|
+
: ''
|
|
39
|
+
return text(`Fields for ${code} (spec "${code}.<field>", or "${code}.person" for a full record):\n${fieldLines}${presetLines}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate synthetic data.
|
|
44
|
+
* spec: "<country>.<selector>" — selector is a field key, "person", or "preset:<key>"
|
|
45
|
+
* count: number of rows (omit or 1 for a single value/record)
|
|
46
|
+
* fields: array of field keys, or "all" (applies to person records)
|
|
47
|
+
* seed: string for reproducible output
|
|
48
|
+
* format: "json" (default) or "csv"
|
|
49
|
+
*/
|
|
50
|
+
export function generateData({ spec, count, fields, seed, format }) {
|
|
51
|
+
if (!spec || typeof spec !== 'string') return fail('`spec` is required, e.g. "ke.person" or "de.iban".')
|
|
52
|
+
const settings = {}
|
|
53
|
+
if (seed != null && seed !== '') settings.seed = String(seed)
|
|
54
|
+
if (fields != null) settings.fields = fields === 'all' ? 'all' : (Array.isArray(fields) ? fields : String(fields).split(',').map(s => s.trim()).filter(Boolean))
|
|
55
|
+
|
|
56
|
+
let data
|
|
57
|
+
try {
|
|
58
|
+
const n = Number(count) || 0
|
|
59
|
+
data = n > 1 ? generateMany(spec, n, settings) : generate(spec, settings)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return fail(e.message)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (format === 'csv') return text(toCsv(data))
|
|
65
|
+
return text(typeof data === 'string' ? data : JSON.stringify(data, null, 2))
|
|
66
|
+
}
|