@prooftell/mcp 0.0.0-stage → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProofTell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # @prooftell/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [ProofTell](https://prooftell.com/integrations?utm_source=mcp&utm_medium=integration)'s MCP server: the checks of
4
+ ProofTell's public API (https://prooftell.com/docs) as tools for Claude, Cursor, VS Code and any client of the
5
+ Model Context Protocol. Ask the assistant to check a phone number, an email address or an IP address, or to assess
6
+ the risk of a sign-up; it calls ProofTell with your key and reads the answer. The server runs on your machine
7
+ (stdio) and stores nothing.
8
+
9
+ ## The key
10
+
11
+ Create a key in the ProofTell dashboard (Developers → Integrations), named after this platform. A `pt_test_` key is
12
+ the free sandbox: simulated, deterministic answers. A `pt_live_` key spends the account's balance. The server reads
13
+ it from the environment variable `PROOFTELL_API_KEY`, set in the client's configuration below.
14
+
15
+ ## Adding it to a client
16
+
17
+ It needs Node.js 18 or later; `npx` fetches the server on first use.
18
+
19
+ **Claude Code:**
20
+
21
+ ```sh
22
+ claude mcp add prooftell --env PROOFTELL_API_KEY=pt_test_… -- npx -y @prooftell/mcp
23
+ ```
24
+
25
+ **Claude Desktop:** Settings → Developer → Edit config, then in `claude_desktop_config.json`:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "prooftell": {
31
+ "command": "npx",
32
+ "args": ["-y", "@prooftell/mcp"],
33
+ "env": { "PROOFTELL_API_KEY": "pt_test_…" }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ **Cursor:** the same block in `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (one project).
40
+
41
+ **VS Code** (agent mode): in `.vscode/mcp.json`; VS Code asks for the key once and keeps it out of the file.
42
+
43
+ ```json
44
+ {
45
+ "inputs": [
46
+ { "type": "promptString", "id": "prooftell-key", "description": "ProofTell API key", "password": true }
47
+ ],
48
+ "servers": {
49
+ "prooftell": {
50
+ "type": "stdio",
51
+ "command": "npx",
52
+ "args": ["-y", "@prooftell/mcp"],
53
+ "env": { "PROOFTELL_API_KEY": "${input:prooftell-key}" }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## The tools
60
+
61
+ Each check is charged per check from your balance; the sandbox is free. The answer is the API's JSON, unchanged.
62
+
63
+ | Tool | Title | Call | Arguments |
64
+ |---|---|---|---|
65
+ | `look_up_phone_number` | Look up a phone number | `POST /v1/phone` | `phone` (required); `country`, for a number in national format; `level`: `basic`, `line_type` (the default), `current_carrier`, `line_status`, `fraud_score`, each including the ones before it |
66
+ | `verify_email_address` | Verify an email address | `POST /v1/email` | `email` (required); `timeout`, 1000 to 45000 ms |
67
+ | `look_up_ip_address` | Look up an IP address | `POST /v1/ip` | `ip` (required) |
68
+ | `assess_identity_risk` | Assess the risk of an identity | `POST /v1/assess` | any of `phone`, `email`, `ip` (one at least); `country`; `reference` |
69
+ | `get_balance` | Get the account's balance | `GET /v1/balance` | — (free) |
70
+
71
+ An assessment answers a `score` from 0 to 100, a `verdict` (allow, review, deny) and the `reasons` that added to the
72
+ score; each signal's own result is under `signals`. A failed call comes back as a tool error with the API's message
73
+ and code — `insufficient_balance` names what the check needs and what to top up, `rate_limited` is a burst above the
74
+ key's rate, `invalid_api_key` a wrong or revoked key — so the assistant can say what to do.
75
+
76
+ ## The sandbox
77
+
78
+ With a `pt_test_` key every call is simulated, deterministic and free: `+447700900123` is a GB mobile,
79
+ `+15005550003` is not valid; `jane@example.com` is deliverable, `disposable@sandbox.prooftell.com` disposable;
80
+ `203.0.113.1` is a clean US address, `203.0.113.66` Tor; an assessment of `+447700900123` with `203.0.113.1` is a
81
+ `geo_mismatch`, allowed with a score of 15. The full list is in the API reference: https://prooftell.com/docs.
82
+
83
+ ## Development
84
+
85
+ Plain JavaScript (ES modules), no build step: `src/index.js` (the server), `src/tools.js` (the tools and their
86
+ schemas), `src/api.js` (the requests).
87
+
88
+ ```sh
89
+ npm install
90
+ npm test # the SDK's client spawns the server: the tools' list, the requests against a local stand-in for the
91
+ # API, the errors; with PROOFTELL_TEST_KEY set to a sandbox key (read into the command, never written
92
+ # down), every tool against staging as well
93
+ ```
94
+
95
+ `PROOFTELL_BASE_URL` points the server at another API (`https://api.prooftell.dev`, staging); it is read from the
96
+ environment only.
97
+
98
+ ## Releasing
99
+
100
+ 1. npm, by GitHub Actions with provenance (`.github/workflows/release-mcp.yml` at the root of
101
+ [prooftell/integrations](https://github.com/prooftell/integrations)): bump `version` in `package.json` and in
102
+ `server.json` (both places), commit, tag `mcp-v<version>` and push the tag. The first release publishes with the
103
+ repository secret `NPM_TOKEN` (a granular token with Read and write on all packages and Bypass two-factor
104
+ authentication); after it, npm's trusted publishing (the package's settings, Publish access → Trusted Publishers
105
+ → GitHub Actions, owner `prooftell`, repository `integrations`, workflow `release-mcp.yml`, "Allow npm publish"
106
+ ticked), the secret and the token deleted. npm expires a new trusted publisher that no successful publish has used within 2 days (then: delete it,
107
+ create it again and release within 2 days); the token-free path was first proven with 1.0.1.
108
+ 2. The official MCP Registry, once the version is on npm, from this folder with
109
+ [mcp-publisher](https://github.com/modelcontextprotocol/registry/releases): `mcp-publisher login dns --domain
110
+ prooftell.com --private-key <the key's hex>`, then `mcp-publisher publish`. The name `com.prooftell/mcp` is proven
111
+ by the TXT record `v=MCPv1; k=ed25519; p=…` at prooftell.com's apex; `package.json`'s `mcpName` ties the npm
112
+ package to it. A version published there cannot be changed or removed: a fix is a new version.
113
+
114
+ ## Resources
115
+
116
+ - ProofTell: https://prooftell.com/integrations?utm_source=mcp&utm_medium=integration
117
+ - The API reference: https://prooftell.com/docs
118
+ - Support: support@prooftell.com
119
+
120
+ ## License
121
+
122
+ MIT.
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@prooftell/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "ProofTell's MCP server: phone, email and IP checks and an identity's risk score as tools for Claude, Cursor, VS Code and any MCP client, with one API key.",
5
+ "license": "MIT",
6
+ "mcpName": "com.prooftell/mcp",
7
+ "homepage": "https://prooftell.com/integrations?utm_source=mcp&utm_medium=integration",
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "prooftell",
12
+ "phone-validation",
13
+ "email-verification",
14
+ "ip-intelligence",
15
+ "fraud"
16
+ ],
17
+ "author": {
18
+ "name": "ProofTell",
19
+ "email": "support@prooftell.com"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/prooftell/integrations.git",
24
+ "directory": "mcp"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/prooftell/integrations/issues",
28
+ "email": "support@prooftell.com"
29
+ },
30
+ "type": "module",
31
+ "bin": {
32
+ "prooftell-mcp": "src/index.js"
33
+ },
34
+ "files": [
35
+ "src"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "test": "node --test test/*.test.js"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.32.2",
45
+ "zod": "^4.0.0"
46
+ }
47
+ }
package/src/api.js ADDED
@@ -0,0 +1,67 @@
1
+ // ProofTell's public API, as the MCP server calls it: one request per tool, the answer back unchanged, or an
2
+ // `ApiError` carrying the api's own message, code and status. The key comes from the server's environment
3
+ // (`PROOFTELL_API_KEY`); `PROOFTELL_BASE_URL` points it elsewhere for tests only. Nothing here logs an input or the key.
4
+ import { readFileSync } from 'node:fs'
5
+
6
+ export const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
7
+
8
+ /** The shipped address. */
9
+ export const BASE_URL = 'https://api.prooftell.com'
10
+
11
+ /** Milliseconds a request may take: an email check may take 45 s at the mail server, plus the way there and back. */
12
+ const REQUEST_TIMEOUT = 60000
13
+
14
+ export class ApiError extends Error {
15
+ constructor (message, { code, status } = {}) {
16
+ super(message)
17
+ this.name = 'ApiError'
18
+ this.code = code
19
+ this.status = status
20
+ }
21
+ }
22
+
23
+ /** Sends one request and resolves with the api's JSON answer. `body` undefined makes it a GET. */
24
+ export async function request (path, body, { env = process.env, fetchImpl = fetch } = {}) {
25
+ const key = (env.PROOFTELL_API_KEY || '').trim()
26
+ if (!key) {
27
+ throw new ApiError(
28
+ 'No API key: set PROOFTELL_API_KEY in this MCP server\'s configuration to a key from the ProofTell dashboard ' +
29
+ '(Developers → Integrations). A pt_test_ key is the free sandbox.',
30
+ { code: 'no_key' },
31
+ )
32
+ }
33
+ const baseUrl = (env.PROOFTELL_BASE_URL || BASE_URL).replace(/\/+$/, '')
34
+ let response
35
+ try {
36
+ response = await fetchImpl(baseUrl + path, {
37
+ method: body === undefined ? 'GET' : 'POST',
38
+ headers: {
39
+ Authorization: `Bearer ${key}`,
40
+ Accept: 'application/json',
41
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
42
+ 'User-Agent': `prooftell-mcp/${version}`,
43
+ },
44
+ body: body === undefined ? undefined : JSON.stringify(body),
45
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT),
46
+ })
47
+ } catch (err) {
48
+ const reason = err && err.name === 'TimeoutError' ? 'it did not answer within 60 seconds' : 'it could not be reached'
49
+ throw new ApiError(`The ProofTell API did not answer: ${reason}.`, { code: 'network' })
50
+ }
51
+ const text = await response.text()
52
+ let answer
53
+ try {
54
+ answer = text ? JSON.parse(text) : {}
55
+ } catch {
56
+ answer = undefined
57
+ }
58
+ if (!response.ok) {
59
+ const error = answer && answer.error
60
+ const message = error && error.message ? error.message : `The ProofTell API answered ${response.status}.`
61
+ throw new ApiError(message, { code: error && error.code, status: response.status })
62
+ }
63
+ if (answer === undefined) {
64
+ throw new ApiError('The ProofTell API answered something other than JSON.', { status: response.status })
65
+ }
66
+ return answer
67
+ }
package/src/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // The ProofTell MCP server: the four checks and the balance as tools, over stdio, for an assistant that runs it
3
+ // locally (Claude, Cursor, VS Code…). Configured by its environment: PROOFTELL_API_KEY, a key from the ProofTell
4
+ // dashboard (Developers → Integrations). Nothing is written to stdout but the protocol; nothing is logged.
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
+ import { version } from './api.js'
8
+ import { TOOLS, run } from './tools.js'
9
+
10
+ const INSTRUCTIONS = `ProofTell checks phone numbers, email addresses and IP addresses, and scores the risk of an identity from any
11
+ mix of the three (0–100, with a verdict: allow, review or deny, and its reasons). Each check is charged per check from
12
+ the account's balance; a pt_test_ key is the free sandbox, whose answers are simulated and deterministic:
13
+ +447700900123 is a GB mobile, jane@example.com is deliverable, 203.0.113.1 is a clean US address, 203.0.113.66 is Tor.
14
+ Use assess_identity_risk to decide on a sign-up or an order; the single checks to clean or enrich one value. The API
15
+ reference: https://prooftell.com/docs.`
16
+
17
+ const server = new McpServer({ name: 'prooftell', title: 'ProofTell', version }, { instructions: INSTRUCTIONS })
18
+
19
+ for (const tool of TOOLS) {
20
+ server.registerTool(
21
+ tool.name,
22
+ { title: tool.title, description: tool.description, inputSchema: tool.inputSchema, annotations: tool.annotations },
23
+ (args) => run(tool, args),
24
+ )
25
+ }
26
+
27
+ await server.connect(new StdioServerTransport())
package/src/tools.js ADDED
@@ -0,0 +1,132 @@
1
+ // The tools an assistant sees: the four checks, named as on every ProofTell connector (their titles; the names are
2
+ // the same in snake_case, as MCP clients expect), and the account's balance. Each check is one request, charged per
3
+ // check from the balance (the sandbox is free); the answer is the api's JSON, unchanged, as text and as structured
4
+ // content. A failed call is a tool error carrying the api's message and code, so the assistant can say what to do.
5
+ import { z } from 'zod'
6
+ import { ApiError, request } from './api.js'
7
+
8
+ const LEVELS = ['basic', 'line_type', 'current_carrier', 'line_status', 'fraud_score']
9
+
10
+ /** A text argument: a required one must not be empty; an optional one may be, and is then not sent. */
11
+ function text (max, description, required) {
12
+ return required ? z.string().min(1).max(max).describe(description) : z.string().max(max).describe(description).optional()
13
+ }
14
+
15
+ const PHONE = 'The phone number, in international format (E.164, e.g. +447700900123), or in national format with the country.'
16
+ const EMAIL = 'The email address, e.g. jane@example.com.'
17
+ const IP = 'The IPv4 or IPv6 address of the end user, e.g. 203.0.113.1.'
18
+ const country = z
19
+ .string()
20
+ .regex(/^([A-Za-z]{2})?$/, 'a two-letter country code')
21
+ .describe('The two-letter country code (ISO 3166-1 alpha-2, e.g. GB) of a number given in national format.')
22
+ .optional()
23
+
24
+ /** The answer of a call that cost something: neither read-only nor idempotent, since each call is charged. */
25
+ const CHECK = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
26
+
27
+ export const TOOLS = [
28
+ {
29
+ name: 'look_up_phone_number',
30
+ title: 'Look up a phone number',
31
+ description:
32
+ 'Checks a phone number: valid or not, its formats and country, then by level its line type, the carrier it is ' +
33
+ 'on today, whether it is live on the network, or a fraud score. The levels are a ladder, each including the ' +
34
+ 'ones before it and priced on its own: basic, line_type (the default), current_carrier, line_status, ' +
35
+ 'fraud_score; what is not offered for the number comes back as unavailable, at no cost. Charged per check from ' +
36
+ 'your balance; the sandbox (a pt_test_ key) is free.',
37
+ inputSchema: {
38
+ phone: text(32, PHONE, true),
39
+ country,
40
+ level: z.enum(LEVELS).optional().describe('How far to look; line_type when omitted.'),
41
+ },
42
+ annotations: { title: 'Look up a phone number', ...CHECK },
43
+ call: (args) => request('/v1/phone', args),
44
+ },
45
+ {
46
+ name: 'verify_email_address',
47
+ title: 'Verify an email address',
48
+ description:
49
+ 'Checks whether an email address accepts mail: a verdict (deliverable, undeliverable, risky, unknown) with its ' +
50
+ 'reason, the flags (disposable, role address, catch-all…), the safe action for a sign-up form, and a likely ' +
51
+ 'correction of a misspelt domain. Charged per check from your balance; the sandbox (a pt_test_ key) is free.',
52
+ inputSchema: {
53
+ email: text(254, EMAIL, true),
54
+ timeout: z
55
+ .number()
56
+ .int()
57
+ .min(1000)
58
+ .max(45000)
59
+ .optional()
60
+ .describe('Milliseconds allowed for the mail server, 1000 to 45000 (the default); past it the address comes back unknown, free.'),
61
+ },
62
+ annotations: { title: 'Verify an email address', ...CHECK },
63
+ call: (args) => request('/v1/email', args),
64
+ },
65
+ {
66
+ name: 'look_up_ip_address',
67
+ title: 'Look up an IP address',
68
+ description:
69
+ 'Finds where an IP address is and what network it is on (country, city, ASN and organisation), and whether it ' +
70
+ 'is Tor, a VPN or a datacenter. Charged per check from your balance; the sandbox (a pt_test_ key) is free.',
71
+ inputSchema: { ip: text(64, IP, true) },
72
+ annotations: { title: 'Look up an IP address', ...CHECK },
73
+ call: (args) => request('/v1/ip', args),
74
+ },
75
+ {
76
+ name: 'assess_identity_risk',
77
+ title: 'Assess the risk of an identity',
78
+ description:
79
+ 'Scores an identity from 0 (no risk found) to 100 with a verdict (allow, review or deny) and the reasons that ' +
80
+ 'added to the score, from any mix of phone number, email address and IP address, each checked in parallel; ' +
81
+ 'each signal\'s own result is under signals. Give at least one of the three. Charged per signal from your ' +
82
+ 'balance; the sandbox (a pt_test_ key) is free.',
83
+ inputSchema: {
84
+ phone: text(32, PHONE),
85
+ email: text(254, EMAIL),
86
+ ip: text(64, IP),
87
+ country,
88
+ reference: z.string().max(128).optional().describe('Your own id for this check (a sign-up, an order), echoed back.'),
89
+ },
90
+ annotations: { title: 'Assess the risk of an identity', ...CHECK },
91
+ call: (args) => {
92
+ if (!args.phone && !args.email && !args.ip) {
93
+ throw new ApiError('Give at least one of a phone number, an email address and an IP address.', { code: 'nothing_to_assess' })
94
+ }
95
+ return request('/v1/assess', args)
96
+ },
97
+ },
98
+ {
99
+ name: 'get_balance',
100
+ title: "Get the account's balance",
101
+ description:
102
+ "The account's balance in US dollars (paid and promotional money, what is held for running files) and what " +
103
+ "this month's checks cost so far, by signal. Free. Useful before a batch of checks, or to tell a live key " +
104
+ '(pt_live_) from the sandbox.',
105
+ inputSchema: {},
106
+ annotations: { title: "Get the account's balance", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
107
+ call: () => request('/v1/balance'),
108
+ },
109
+ ]
110
+
111
+ /** The arguments as the api takes them: empty strings dropped, a country upper-cased. */
112
+ function cleaned (args = {}) {
113
+ const out = {}
114
+ for (const [key, value] of Object.entries(args)) {
115
+ if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) continue
116
+ out[key] = typeof value === 'string' ? value.trim() : value
117
+ }
118
+ if (out.country) out.country = out.country.toUpperCase()
119
+ return out
120
+ }
121
+
122
+ /** Runs a tool and shapes its result for MCP: the answer as JSON text and structured content, or a tool error. */
123
+ export async function run (tool, args) {
124
+ try {
125
+ const answer = await tool.call(cleaned(args))
126
+ return { content: [{ type: 'text', text: JSON.stringify(answer, null, 2) }], structuredContent: answer }
127
+ } catch (err) {
128
+ const message = err instanceof ApiError ? err.message : 'The call failed before reaching ProofTell.'
129
+ const detail = err instanceof ApiError && err.code ? ` (${err.code}${err.status ? `, HTTP ${err.status}` : ''})` : ''
130
+ return { isError: true, content: [{ type: 'text', text: message + detail }] }
131
+ }
132
+ }