fipsign-mcp 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.
- fipsign_mcp/__init__.py +11 -0
- fipsign_mcp/py.typed +0 -0
- fipsign_mcp/server.py +712 -0
- fipsign_mcp-0.1.0.dist-info/METADATA +262 -0
- fipsign_mcp-0.1.0.dist-info/RECORD +9 -0
- fipsign_mcp-0.1.0.dist-info/WHEEL +5 -0
- fipsign_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- fipsign_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- fipsign_mcp-0.1.0.dist-info/top_level.txt +1 -0
fipsign_mcp/__init__.py
ADDED
fipsign_mcp/py.typed
ADDED
|
File without changes
|
fipsign_mcp/server.py
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fipsign_mcp.server — MCP server for FIPSign post-quantum signing API.
|
|
3
|
+
|
|
4
|
+
Exposes 15 tools covering the full FIPSign runtime API:
|
|
5
|
+
signing, verification, revocation, usage, CA certificate lifecycle,
|
|
6
|
+
key pair generation, and webhook management.
|
|
7
|
+
|
|
8
|
+
Configuration (environment variables):
|
|
9
|
+
FIPSIGN_API_KEY — required for most tools (pqa_ + 64 hex chars)
|
|
10
|
+
FIPSIGN_BASE_URL — optional, defaults to https://api.fipsign.dev
|
|
11
|
+
|
|
12
|
+
Transport: stdio (compatible with Claude Desktop and Claude Code)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
from mcp.server import Server
|
|
25
|
+
from mcp.server.stdio import stdio_server
|
|
26
|
+
from mcp.types import (
|
|
27
|
+
CallToolResult,
|
|
28
|
+
TextContent,
|
|
29
|
+
Tool,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ─── Configuration ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
API_KEY = os.environ.get("FIPSIGN_API_KEY", "")
|
|
35
|
+
BASE_URL = os.environ.get("FIPSIGN_BASE_URL", "https://api.fipsign.dev").rstrip("/")
|
|
36
|
+
|
|
37
|
+
# ─── HTTP helper ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
async def api_request(
|
|
40
|
+
method: str,
|
|
41
|
+
path: str,
|
|
42
|
+
body: Any = None,
|
|
43
|
+
) -> tuple[bool, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Make an authenticated request to the FIPSign API.
|
|
46
|
+
Returns (ok: bool, data: Any).
|
|
47
|
+
"""
|
|
48
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
49
|
+
if API_KEY:
|
|
50
|
+
headers["X-API-Key"] = API_KEY
|
|
51
|
+
|
|
52
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
53
|
+
try:
|
|
54
|
+
response = await client.request(
|
|
55
|
+
method,
|
|
56
|
+
f"{BASE_URL}{path}",
|
|
57
|
+
headers=headers,
|
|
58
|
+
content=json.dumps(body).encode() if body is not None else None,
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
data = response.json()
|
|
62
|
+
except Exception:
|
|
63
|
+
data = {"success": False, "error": f"HTTP {response.status_code} — non-JSON response"}
|
|
64
|
+
return response.is_success, data
|
|
65
|
+
except httpx.TimeoutException:
|
|
66
|
+
return False, {"success": False, "error": "Request timed out after 30 seconds"}
|
|
67
|
+
except httpx.NetworkError as exc:
|
|
68
|
+
return False, {"success": False, "error": f"Network error: {exc}"}
|
|
69
|
+
|
|
70
|
+
# ─── Key pair generation ──────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def _generate_key_pair() -> dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Generate an ML-DSA-65 key pair using pyca/cryptography >= 48.0.0.
|
|
75
|
+
Returns publicKey (base64, 1952 bytes) and secretKey (base64, 32-byte seed).
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA65PrivateKey
|
|
79
|
+
except ImportError:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
"generate_key_pair requires cryptography >= 48.0.0. "
|
|
82
|
+
"Install with: pip install 'cryptography>=48.0.0'"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
private_key = MLDSA65PrivateKey.generate()
|
|
86
|
+
public_key = private_key.public_key()
|
|
87
|
+
|
|
88
|
+
pub_b64 = base64.b64encode(public_key.public_bytes_raw()).decode()
|
|
89
|
+
seed_b64 = base64.b64encode(private_key.private_bytes_raw()).decode()
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"publicKey": pub_b64,
|
|
93
|
+
"secretKey": seed_b64,
|
|
94
|
+
"algorithm": "ML-DSA-65",
|
|
95
|
+
"standard": "NIST FIPS 204",
|
|
96
|
+
"sizes": {
|
|
97
|
+
"publicKeyBytes": len(public_key.public_bytes_raw()),
|
|
98
|
+
"secretKeyBytes": len(private_key.private_bytes_raw()),
|
|
99
|
+
"note": "secretKey is the 32-byte seed form (Python SDK convention). The publicKey is 1952 bytes — compatible with fipsign_ca_issue and the JS SDK.",
|
|
100
|
+
},
|
|
101
|
+
"note": "Store secretKey securely on the device. Never send it to any server. Pass publicKey to fipsign_ca_issue.",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ─── Response helpers ─────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
def _ok(data: Any) -> CallToolResult:
|
|
107
|
+
return CallToolResult(
|
|
108
|
+
content=[TextContent(type="text", text=json.dumps(data, indent=2))]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _err(message: str, detail: Any = None) -> CallToolResult:
|
|
112
|
+
payload: dict[str, Any] = {"error": message}
|
|
113
|
+
if detail is not None:
|
|
114
|
+
payload["detail"] = detail
|
|
115
|
+
return CallToolResult(
|
|
116
|
+
content=[TextContent(type="text", text=json.dumps(payload, indent=2))],
|
|
117
|
+
isError=True,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _missing_api_key() -> CallToolResult:
|
|
121
|
+
return _err(
|
|
122
|
+
"FIPSIGN_API_KEY is not set. "
|
|
123
|
+
"Export it before starting the server: export FIPSIGN_API_KEY=pqa_..."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# ─── Tool definitions ─────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
TOOLS: list[Tool] = [
|
|
129
|
+
# ── Infrastructure ─────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
Tool(
|
|
132
|
+
name="fipsign_health",
|
|
133
|
+
description=(
|
|
134
|
+
"Check the health of the FIPSign service. Returns the service status, "
|
|
135
|
+
"algorithm (ML-DSA-65), NIST standard, and version. No API key required. "
|
|
136
|
+
"Use this to verify the service is reachable before running other operations."
|
|
137
|
+
),
|
|
138
|
+
inputSchema={
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {},
|
|
141
|
+
"required": [],
|
|
142
|
+
},
|
|
143
|
+
),
|
|
144
|
+
|
|
145
|
+
Tool(
|
|
146
|
+
name="fipsign_public_key",
|
|
147
|
+
description=(
|
|
148
|
+
"Get the current ML-DSA-65 public key of the FIPSign server. Returns a "
|
|
149
|
+
"base64-encoded 1952-byte public key. Use this when you need to verify token "
|
|
150
|
+
"signatures independently without calling the /verify endpoint (e.g. for "
|
|
151
|
+
"offline verification or third-party auditing). No API key required."
|
|
152
|
+
),
|
|
153
|
+
inputSchema={
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {},
|
|
156
|
+
"required": [],
|
|
157
|
+
},
|
|
158
|
+
),
|
|
159
|
+
|
|
160
|
+
# ── Core signing ────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
Tool(
|
|
163
|
+
name="fipsign_sign",
|
|
164
|
+
description=(
|
|
165
|
+
"Sign any payload with ML-DSA-65 (NIST FIPS 204). The only required field is "
|
|
166
|
+
"'sub' — any string identifying the entity being signed: a user ID, order ID, "
|
|
167
|
+
"document hash, device serial, AI agent action, or anything else. All other "
|
|
168
|
+
"fields are stored in the payload and returned on verify. Costs 1 token. "
|
|
169
|
+
"Returns the signed token object (payload, signature, algorithm, issuedAt) "
|
|
170
|
+
"plus usage info."
|
|
171
|
+
),
|
|
172
|
+
inputSchema={
|
|
173
|
+
"type": "object",
|
|
174
|
+
"properties": {
|
|
175
|
+
"sub": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"description": (
|
|
178
|
+
"Required. Entity identifier. Max 128 characters. "
|
|
179
|
+
"Examples: 'user_123', 'order_456', 'doc_hash_abc', "
|
|
180
|
+
"'device_serial_001', 'agent_action_summarize'."
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
"expiresInSeconds": {
|
|
184
|
+
"type": "number",
|
|
185
|
+
"description": (
|
|
186
|
+
"Token lifetime in seconds. Default: 3600 (1 hour). "
|
|
187
|
+
"Pass a larger value for long-lived tokens (e.g. document signatures: "
|
|
188
|
+
"365 * 24 * 3600)."
|
|
189
|
+
),
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
"required": ["sub"],
|
|
193
|
+
"additionalProperties": {
|
|
194
|
+
"description": (
|
|
195
|
+
"Any additional custom fields to embed in the payload. "
|
|
196
|
+
"Max 10 extra fields, string values max 256 chars."
|
|
197
|
+
),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
),
|
|
201
|
+
|
|
202
|
+
Tool(
|
|
203
|
+
name="fipsign_verify",
|
|
204
|
+
description=(
|
|
205
|
+
"Verify a FIPSign token signed with ML-DSA-65. Checks the cryptographic "
|
|
206
|
+
"signature, expiry, and revocation list. Returns valid:true with the decoded "
|
|
207
|
+
"payload on success, or valid:false with an error message on failure. "
|
|
208
|
+
"Never throws — always returns a result. Costs 1 token."
|
|
209
|
+
),
|
|
210
|
+
inputSchema={
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"token": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"description": (
|
|
216
|
+
"The token object returned by fipsign_sign. "
|
|
217
|
+
"Must have: payload (string), signature (string), "
|
|
218
|
+
"algorithm (string), issuedAt (number)."
|
|
219
|
+
),
|
|
220
|
+
"properties": {
|
|
221
|
+
"payload": {"type": "string"},
|
|
222
|
+
"signature": {"type": "string"},
|
|
223
|
+
"algorithm": {"type": "string"},
|
|
224
|
+
"issuedAt": {"type": "number"},
|
|
225
|
+
},
|
|
226
|
+
"required": ["payload", "signature", "algorithm", "issuedAt"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
"required": ["token"],
|
|
230
|
+
},
|
|
231
|
+
),
|
|
232
|
+
|
|
233
|
+
Tool(
|
|
234
|
+
name="fipsign_revoke",
|
|
235
|
+
description=(
|
|
236
|
+
"Permanently revoke a token. Once revoked, all future verify() calls will "
|
|
237
|
+
"reject the token even if its signature is valid and it has not expired. "
|
|
238
|
+
"Idempotent: revoking an already-revoked token returns success without "
|
|
239
|
+
"consuming an extra token. Costs 1 token. "
|
|
240
|
+
"Note: calling this on an already-expired token returns an error (400)."
|
|
241
|
+
),
|
|
242
|
+
inputSchema={
|
|
243
|
+
"type": "object",
|
|
244
|
+
"properties": {
|
|
245
|
+
"token": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"description": "The token object to revoke. Must have: payload, signature, algorithm, issuedAt.",
|
|
248
|
+
"properties": {
|
|
249
|
+
"payload": {"type": "string"},
|
|
250
|
+
"signature": {"type": "string"},
|
|
251
|
+
"algorithm": {"type": "string"},
|
|
252
|
+
"issuedAt": {"type": "number"},
|
|
253
|
+
},
|
|
254
|
+
"required": ["payload", "signature", "algorithm", "issuedAt"],
|
|
255
|
+
},
|
|
256
|
+
"reason": {
|
|
257
|
+
"type": "string",
|
|
258
|
+
"description": (
|
|
259
|
+
"Optional human-readable reason stored server-side. "
|
|
260
|
+
"Examples: 'user logged out', 'order cancelled', "
|
|
261
|
+
"'suspicious activity detected'."
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
"required": ["token"],
|
|
266
|
+
},
|
|
267
|
+
),
|
|
268
|
+
|
|
269
|
+
# ── Account ─────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
Tool(
|
|
272
|
+
name="fipsign_usage",
|
|
273
|
+
description=(
|
|
274
|
+
"Get the current token balance and 6-month usage history for this API key's "
|
|
275
|
+
"account. Returns free tokens remaining (resets monthly), pack tokens remaining "
|
|
276
|
+
"(never expire), total remaining, and a monthly breakdown. "
|
|
277
|
+
"Free — no token cost. Use before batch operations to confirm sufficient balance."
|
|
278
|
+
),
|
|
279
|
+
inputSchema={
|
|
280
|
+
"type": "object",
|
|
281
|
+
"properties": {},
|
|
282
|
+
"required": [],
|
|
283
|
+
},
|
|
284
|
+
),
|
|
285
|
+
|
|
286
|
+
# ── Key generation ──────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
Tool(
|
|
289
|
+
name="fipsign_generate_key_pair",
|
|
290
|
+
description=(
|
|
291
|
+
"Generate an ML-DSA-65 key pair locally (no API call, no token cost). "
|
|
292
|
+
"Returns a base64-encoded public key (1952 bytes) and secret key (32-byte seed). "
|
|
293
|
+
"Use the publicKey when calling fipsign_ca_issue to certify a device or entity. "
|
|
294
|
+
"SECURITY WARNING: the secretKey is sensitive — store it securely on the device "
|
|
295
|
+
"and never send it to any server. The secretKey will appear in this tool's "
|
|
296
|
+
"response; treat it like a private key.\n\n"
|
|
297
|
+
"Note: The Python SDK returns the secretKey as a 32-byte seed (base64). "
|
|
298
|
+
"This differs from the JS SDK which returns a 4032-byte expanded key. "
|
|
299
|
+
"Both publicKeys (1952 bytes) are fully interoperable with the FIPSign backend."
|
|
300
|
+
),
|
|
301
|
+
inputSchema={
|
|
302
|
+
"type": "object",
|
|
303
|
+
"properties": {},
|
|
304
|
+
"required": [],
|
|
305
|
+
},
|
|
306
|
+
),
|
|
307
|
+
|
|
308
|
+
# ── Certificate Authority ───────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
Tool(
|
|
311
|
+
name="fipsign_ca_issue",
|
|
312
|
+
description=(
|
|
313
|
+
"Issue a post-quantum certificate signed by the project's CA. The certificate "
|
|
314
|
+
"certifies that the entity identified by 'subject' controls the given ML-DSA-65 "
|
|
315
|
+
"public key. Supports both PQCert (native JSON) and X.509 (standard PEM) CA "
|
|
316
|
+
"formats — the format is determined by which CA type was created in the "
|
|
317
|
+
"dashboard. For PQCert CAs, the response includes a certificate JSON object. "
|
|
318
|
+
"For X.509 CAs, it includes a PEM string. Costs 1 token.\n\n"
|
|
319
|
+
"Required: subject (entity name/ID), publicKey (base64 ML-DSA-65 public key — "
|
|
320
|
+
"generate with fipsign_generate_key_pair), expiresInSeconds "
|
|
321
|
+
"(min 60, max 157680000 = 5 years).\n\n"
|
|
322
|
+
"Optional: meta (up to 10 key-value pairs — PQCert CAs only; passing meta "
|
|
323
|
+
"to an X.509 CA returns a 400 error).\n\n"
|
|
324
|
+
"The returned certId (in meta.certId) is what you need for "
|
|
325
|
+
"fipsign_ca_revoke_cert and fipsign_ca_get_cert."
|
|
326
|
+
),
|
|
327
|
+
inputSchema={
|
|
328
|
+
"type": "object",
|
|
329
|
+
"properties": {
|
|
330
|
+
"subject": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"description": (
|
|
333
|
+
"Entity identifier to certify. Examples: 'device-serial-00123', "
|
|
334
|
+
"'service-payment-processor', 'lock-v3-batch-2026'. Max 256 characters."
|
|
335
|
+
),
|
|
336
|
+
},
|
|
337
|
+
"publicKey": {
|
|
338
|
+
"type": "string",
|
|
339
|
+
"description": (
|
|
340
|
+
"Base64-encoded ML-DSA-65 public key of the entity to certify "
|
|
341
|
+
"(1952 bytes decoded). Generate with fipsign_generate_key_pair."
|
|
342
|
+
),
|
|
343
|
+
},
|
|
344
|
+
"expiresInSeconds": {
|
|
345
|
+
"type": "number",
|
|
346
|
+
"description": (
|
|
347
|
+
"Certificate lifetime in seconds. Min: 60 (1 minute). "
|
|
348
|
+
"Max: 157680000 (5 years). Example: 31536000 = 1 year."
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
"meta": {
|
|
352
|
+
"type": "object",
|
|
353
|
+
"description": (
|
|
354
|
+
"Optional custom key-value pairs to embed in the certificate "
|
|
355
|
+
"(PQCert CAs only — returns 400 for X.509 CAs). Max 10 keys. "
|
|
356
|
+
'Example: {"model": "lock-v3", "batch": "2026-05"}.'
|
|
357
|
+
),
|
|
358
|
+
"additionalProperties": True,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
"required": ["subject", "publicKey", "expiresInSeconds"],
|
|
362
|
+
},
|
|
363
|
+
),
|
|
364
|
+
|
|
365
|
+
Tool(
|
|
366
|
+
name="fipsign_ca_revoke_cert",
|
|
367
|
+
description=(
|
|
368
|
+
"Revoke a certificate immediately. From this point on, the certificate will "
|
|
369
|
+
"appear in the CRL returned by fipsign_ca_get_crl. Use fipsign_ca_get_cert to "
|
|
370
|
+
"check real-time revocation status of a single certificate. Costs 1 token. "
|
|
371
|
+
"Returns 409 if the certificate is already revoked."
|
|
372
|
+
),
|
|
373
|
+
inputSchema={
|
|
374
|
+
"type": "object",
|
|
375
|
+
"properties": {
|
|
376
|
+
"certId": {
|
|
377
|
+
"type": "string",
|
|
378
|
+
"description": (
|
|
379
|
+
"The certificate ID to revoke (cert_...). "
|
|
380
|
+
"For PQCert: the 'id' field of the certificate object. "
|
|
381
|
+
"For X.509: the 'certId' field from meta returned by fipsign_ca_issue."
|
|
382
|
+
),
|
|
383
|
+
},
|
|
384
|
+
"reason": {
|
|
385
|
+
"type": "string",
|
|
386
|
+
"description": (
|
|
387
|
+
"Optional reason for revocation. Max 256 characters. "
|
|
388
|
+
"Examples: 'device decommissioned', 'device reported stolen', "
|
|
389
|
+
"'key compromise'."
|
|
390
|
+
),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
"required": ["certId"],
|
|
394
|
+
},
|
|
395
|
+
),
|
|
396
|
+
|
|
397
|
+
Tool(
|
|
398
|
+
name="fipsign_ca_get_cert",
|
|
399
|
+
description=(
|
|
400
|
+
"Get a certificate by ID and its current real-time status "
|
|
401
|
+
"(revoked, expired, revokedAt, expiresAt). Use this for single certificate "
|
|
402
|
+
"checks before authorizing high-value operations. For bulk offline revocation "
|
|
403
|
+
"checks across many certificates, use fipsign_ca_get_crl instead. "
|
|
404
|
+
"Free — no token cost."
|
|
405
|
+
),
|
|
406
|
+
inputSchema={
|
|
407
|
+
"type": "object",
|
|
408
|
+
"properties": {
|
|
409
|
+
"certId": {
|
|
410
|
+
"type": "string",
|
|
411
|
+
"description": (
|
|
412
|
+
"The certificate ID (cert_...). "
|
|
413
|
+
"For PQCert: certificate.id. "
|
|
414
|
+
"For X.509: meta.certId from fipsign_ca_issue."
|
|
415
|
+
),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
"required": ["certId"],
|
|
419
|
+
},
|
|
420
|
+
),
|
|
421
|
+
|
|
422
|
+
Tool(
|
|
423
|
+
name="fipsign_ca_get_crl",
|
|
424
|
+
description=(
|
|
425
|
+
"Get the Certificate Revocation List (CRL) for this project's CA. Returns all "
|
|
426
|
+
"revoked certificate IDs with their revocation timestamps and reasons. Use this "
|
|
427
|
+
"to check revocation status of multiple certificates offline — download once, "
|
|
428
|
+
"check locally. For a single certificate's real-time status use "
|
|
429
|
+
"fipsign_ca_get_cert instead. Free — no token cost. For X.509 CAs the CRL is "
|
|
430
|
+
"signed with ML-DSA-65 and includes the full signed object in the 'raw' field."
|
|
431
|
+
),
|
|
432
|
+
inputSchema={
|
|
433
|
+
"type": "object",
|
|
434
|
+
"properties": {},
|
|
435
|
+
"required": [],
|
|
436
|
+
},
|
|
437
|
+
),
|
|
438
|
+
|
|
439
|
+
# ── Webhooks ─────────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
Tool(
|
|
442
|
+
name="fipsign_webhooks_register",
|
|
443
|
+
description=(
|
|
444
|
+
"Register or update a webhook endpoint that will receive real-time event "
|
|
445
|
+
"notifications. Available events: 'token.signed', 'token.rejected', "
|
|
446
|
+
"'token.revoked', 'limit.warning' (fired at 20% free tokens remaining), "
|
|
447
|
+
"'limit.reached' (fired when free tokens are exhausted). If omitted, all events "
|
|
448
|
+
"are subscribed. Re-registering an existing webhook updates the URL and events "
|
|
449
|
+
"but preserves the original secret — to rotate the secret, delete and "
|
|
450
|
+
"re-register. The 'secret' field in the response is shown only once — store it "
|
|
451
|
+
"securely to verify incoming request signatures via HMAC-SHA256 on the "
|
|
452
|
+
"X-PQAuth-Signature header."
|
|
453
|
+
),
|
|
454
|
+
inputSchema={
|
|
455
|
+
"type": "object",
|
|
456
|
+
"properties": {
|
|
457
|
+
"url": {
|
|
458
|
+
"type": "string",
|
|
459
|
+
"description": (
|
|
460
|
+
"HTTPS endpoint that will receive POST requests. Must be a valid "
|
|
461
|
+
"HTTPS URL accessible from the internet. "
|
|
462
|
+
"Example: 'https://yourapp.com/webhooks/fipsign'."
|
|
463
|
+
),
|
|
464
|
+
},
|
|
465
|
+
"events": {
|
|
466
|
+
"type": "array",
|
|
467
|
+
"items": {
|
|
468
|
+
"type": "string",
|
|
469
|
+
"enum": [
|
|
470
|
+
"token.signed",
|
|
471
|
+
"token.rejected",
|
|
472
|
+
"token.revoked",
|
|
473
|
+
"limit.warning",
|
|
474
|
+
"limit.reached",
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
"description": (
|
|
478
|
+
"Optional list of events to subscribe to. "
|
|
479
|
+
"Defaults to all events if omitted."
|
|
480
|
+
),
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
"required": ["url"],
|
|
484
|
+
},
|
|
485
|
+
),
|
|
486
|
+
|
|
487
|
+
Tool(
|
|
488
|
+
name="fipsign_webhooks_get",
|
|
489
|
+
description=(
|
|
490
|
+
"Get the current webhook configuration (URL, subscribed events, active status, "
|
|
491
|
+
"creation timestamp). The webhook secret is never returned after initial "
|
|
492
|
+
"registration — only the URL and event list. Returns null webhook if no webhook "
|
|
493
|
+
"has been registered."
|
|
494
|
+
),
|
|
495
|
+
inputSchema={
|
|
496
|
+
"type": "object",
|
|
497
|
+
"properties": {},
|
|
498
|
+
"required": [],
|
|
499
|
+
},
|
|
500
|
+
),
|
|
501
|
+
|
|
502
|
+
Tool(
|
|
503
|
+
name="fipsign_webhooks_delete",
|
|
504
|
+
description=(
|
|
505
|
+
"Delete the current webhook configuration. After deletion, no events will be "
|
|
506
|
+
"delivered until a new webhook is registered via fipsign_webhooks_register."
|
|
507
|
+
),
|
|
508
|
+
inputSchema={
|
|
509
|
+
"type": "object",
|
|
510
|
+
"properties": {},
|
|
511
|
+
"required": [],
|
|
512
|
+
},
|
|
513
|
+
),
|
|
514
|
+
|
|
515
|
+
Tool(
|
|
516
|
+
name="fipsign_webhooks_test",
|
|
517
|
+
description=(
|
|
518
|
+
"Send a test 'token.signed' event to the registered webhook endpoint. Use this "
|
|
519
|
+
"immediately after registering a webhook to confirm delivery is working before "
|
|
520
|
+
"relying on it in production. Requires a webhook to be registered first."
|
|
521
|
+
),
|
|
522
|
+
inputSchema={
|
|
523
|
+
"type": "object",
|
|
524
|
+
"properties": {},
|
|
525
|
+
"required": [],
|
|
526
|
+
},
|
|
527
|
+
),
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
# ─── Tool handlers ────────────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
async def handle_tool(name: str, args: dict[str, Any]) -> CallToolResult:
|
|
533
|
+
# Tools that don't require API key
|
|
534
|
+
if name == "fipsign_health":
|
|
535
|
+
ok, data = await api_request("GET", "/health")
|
|
536
|
+
return _ok(data)
|
|
537
|
+
|
|
538
|
+
if name == "fipsign_public_key":
|
|
539
|
+
ok, data = await api_request("GET", "/public-key")
|
|
540
|
+
return _ok(data)
|
|
541
|
+
|
|
542
|
+
if name == "fipsign_generate_key_pair":
|
|
543
|
+
try:
|
|
544
|
+
result = _generate_key_pair()
|
|
545
|
+
return _ok(result)
|
|
546
|
+
except RuntimeError as exc:
|
|
547
|
+
return _err(str(exc))
|
|
548
|
+
|
|
549
|
+
# All remaining tools require API key
|
|
550
|
+
if not API_KEY:
|
|
551
|
+
return _missing_api_key()
|
|
552
|
+
|
|
553
|
+
if name == "fipsign_sign":
|
|
554
|
+
sub = args.get("sub")
|
|
555
|
+
if not sub or not isinstance(sub, str):
|
|
556
|
+
return _err('"sub" is required and must be a string')
|
|
557
|
+
body: dict[str, Any] = {k: v for k, v in args.items() if k != "expiresInSeconds"}
|
|
558
|
+
if "expiresInSeconds" in args:
|
|
559
|
+
body["expiresInSeconds"] = args["expiresInSeconds"]
|
|
560
|
+
ok_flag, data = await api_request("POST", "/sign", body)
|
|
561
|
+
if not ok_flag:
|
|
562
|
+
return _err("Sign failed", data)
|
|
563
|
+
return _ok(data)
|
|
564
|
+
|
|
565
|
+
if name == "fipsign_verify":
|
|
566
|
+
token = args.get("token")
|
|
567
|
+
if not token or not isinstance(token, dict):
|
|
568
|
+
return _err('"token" is required and must be the token object returned by fipsign_sign')
|
|
569
|
+
ok_flag, data = await api_request("POST", "/verify", {"token": token})
|
|
570
|
+
# verify returns valid:false on failure — not necessarily an HTTP error
|
|
571
|
+
return _ok(data)
|
|
572
|
+
|
|
573
|
+
if name == "fipsign_revoke":
|
|
574
|
+
token = args.get("token")
|
|
575
|
+
if not token or not isinstance(token, dict):
|
|
576
|
+
return _err('"token" is required and must be the token object returned by fipsign_sign')
|
|
577
|
+
body = {"token": token}
|
|
578
|
+
if "reason" in args:
|
|
579
|
+
body["reason"] = args["reason"]
|
|
580
|
+
ok_flag, data = await api_request("POST", "/revoke", body)
|
|
581
|
+
if not ok_flag:
|
|
582
|
+
return _err("Revoke failed", data)
|
|
583
|
+
return _ok(data)
|
|
584
|
+
|
|
585
|
+
if name == "fipsign_usage":
|
|
586
|
+
ok_flag, data = await api_request("GET", "/usage")
|
|
587
|
+
if not ok_flag:
|
|
588
|
+
return _err("Usage request failed", data)
|
|
589
|
+
return _ok(data)
|
|
590
|
+
|
|
591
|
+
if name == "fipsign_ca_issue":
|
|
592
|
+
subject = args.get("subject")
|
|
593
|
+
public_key = args.get("publicKey")
|
|
594
|
+
expires_in_seconds = args.get("expiresInSeconds")
|
|
595
|
+
|
|
596
|
+
if not subject or not isinstance(subject, str):
|
|
597
|
+
return _err('"subject" is required')
|
|
598
|
+
if not public_key or not isinstance(public_key, str):
|
|
599
|
+
return _err('"publicKey" is required — generate one with fipsign_generate_key_pair')
|
|
600
|
+
if not isinstance(expires_in_seconds, (int, float)):
|
|
601
|
+
return _err('"expiresInSeconds" is required and must be a number (min 60, max 157680000)')
|
|
602
|
+
|
|
603
|
+
body = {
|
|
604
|
+
"subject": subject,
|
|
605
|
+
"publicKey": public_key,
|
|
606
|
+
"expiresInSeconds": expires_in_seconds,
|
|
607
|
+
}
|
|
608
|
+
if "meta" in args:
|
|
609
|
+
body["meta"] = args["meta"]
|
|
610
|
+
|
|
611
|
+
ok_flag, data = await api_request("POST", "/ca/issue", body)
|
|
612
|
+
if not ok_flag:
|
|
613
|
+
return _err("CA issue failed", data)
|
|
614
|
+
return _ok(data)
|
|
615
|
+
|
|
616
|
+
if name == "fipsign_ca_revoke_cert":
|
|
617
|
+
cert_id = args.get("certId")
|
|
618
|
+
if not cert_id or not isinstance(cert_id, str):
|
|
619
|
+
return _err('"certId" is required')
|
|
620
|
+
body = {"certId": cert_id}
|
|
621
|
+
if "reason" in args:
|
|
622
|
+
body["reason"] = args["reason"]
|
|
623
|
+
ok_flag, data = await api_request("POST", "/ca/revoke", body)
|
|
624
|
+
if not ok_flag:
|
|
625
|
+
return _err("CA revoke failed", data)
|
|
626
|
+
return _ok(data)
|
|
627
|
+
|
|
628
|
+
if name == "fipsign_ca_get_cert":
|
|
629
|
+
cert_id = args.get("certId")
|
|
630
|
+
if not cert_id or not isinstance(cert_id, str):
|
|
631
|
+
return _err('"certId" is required')
|
|
632
|
+
ok_flag, data = await api_request("GET", f"/ca/certificate/{cert_id}")
|
|
633
|
+
if not ok_flag:
|
|
634
|
+
return _err("CA get cert failed", data)
|
|
635
|
+
return _ok(data)
|
|
636
|
+
|
|
637
|
+
if name == "fipsign_ca_get_crl":
|
|
638
|
+
ok_flag, data = await api_request("GET", "/ca/crl")
|
|
639
|
+
if not ok_flag:
|
|
640
|
+
return _err("CA get CRL failed", data)
|
|
641
|
+
return _ok(data)
|
|
642
|
+
|
|
643
|
+
if name == "fipsign_webhooks_register":
|
|
644
|
+
url = args.get("url")
|
|
645
|
+
if not url or not isinstance(url, str):
|
|
646
|
+
return _err('"url" is required')
|
|
647
|
+
body = {"url": url}
|
|
648
|
+
if "events" in args:
|
|
649
|
+
body["events"] = args["events"]
|
|
650
|
+
ok_flag, data = await api_request("POST", "/webhooks", body)
|
|
651
|
+
if not ok_flag:
|
|
652
|
+
return _err("Webhook register failed", data)
|
|
653
|
+
return _ok(data)
|
|
654
|
+
|
|
655
|
+
if name == "fipsign_webhooks_get":
|
|
656
|
+
ok_flag, data = await api_request("GET", "/webhooks")
|
|
657
|
+
if not ok_flag:
|
|
658
|
+
return _err("Webhook get failed", data)
|
|
659
|
+
return _ok(data)
|
|
660
|
+
|
|
661
|
+
if name == "fipsign_webhooks_delete":
|
|
662
|
+
ok_flag, data = await api_request("DELETE", "/webhooks")
|
|
663
|
+
if not ok_flag:
|
|
664
|
+
return _err("Webhook delete failed", data)
|
|
665
|
+
return _ok(data)
|
|
666
|
+
|
|
667
|
+
if name == "fipsign_webhooks_test":
|
|
668
|
+
ok_flag, data = await api_request("POST", "/webhooks/test")
|
|
669
|
+
if not ok_flag:
|
|
670
|
+
return _err("Webhook test failed", data)
|
|
671
|
+
return _ok(data)
|
|
672
|
+
|
|
673
|
+
return _err(f"Unknown tool: {name}")
|
|
674
|
+
|
|
675
|
+
# ─── Server setup ─────────────────────────────────────────────────────────────
|
|
676
|
+
|
|
677
|
+
app = Server("fipsign-mcp")
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@app.list_tools()
|
|
681
|
+
async def list_tools() -> list[Tool]:
|
|
682
|
+
return TOOLS
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
@app.call_tool()
|
|
686
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
687
|
+
try:
|
|
688
|
+
result = await handle_tool(name, arguments or {})
|
|
689
|
+
return result.content # type: ignore[return-value]
|
|
690
|
+
except Exception as exc:
|
|
691
|
+
error_result = _err(f"Unexpected error in tool '{name}': {exc}")
|
|
692
|
+
return error_result.content # type: ignore[return-value]
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# ─── Entry point ─────────────────────────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
def main() -> None:
|
|
698
|
+
import asyncio
|
|
699
|
+
|
|
700
|
+
async def _run() -> None:
|
|
701
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
702
|
+
await app.run(
|
|
703
|
+
read_stream,
|
|
704
|
+
write_stream,
|
|
705
|
+
app.create_initialization_options(),
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
asyncio.run(_run())
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
if __name__ == "__main__":
|
|
712
|
+
main()
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fipsign-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for FIPSign — post-quantum signing via ML-DSA-65 (NIST FIPS 204)
|
|
5
|
+
Author-email: FIPSign <sdk@fipsign.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://fipsign.dev
|
|
8
|
+
Project-URL: Dashboard, https://app.fipsign.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/fipsign/fipsign-mcp-python
|
|
10
|
+
Keywords: mcp,model-context-protocol,fipsign,post-quantum,ml-dsa,signing,cryptography,nist,fips-204
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: mcp>=1.0.0
|
|
25
|
+
Requires-Dist: cryptography>=48.0.0
|
|
26
|
+
Requires-Dist: httpx>=0.24
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# fipsign-mcp
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/fipsign-mcp/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://csrc.nist.gov/pubs/fips/204/final)
|
|
34
|
+
|
|
35
|
+
MCP server for [FIPSign](https://fipsign.dev) — post-quantum digital signing via **ML-DSA-65** (NIST FIPS 204).
|
|
36
|
+
|
|
37
|
+
Gives Claude Desktop, Claude Code, and any MCP-compatible AI agent full access to the FIPSign API without writing code: sign payloads, verify tokens, issue and revoke post-quantum certificates, manage webhooks, and monitor usage.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Tools
|
|
42
|
+
|
|
43
|
+
| Tool | Description | Token cost |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `fipsign_health` | Check service status | free |
|
|
46
|
+
| `fipsign_public_key` | Get the server's ML-DSA-65 public key | free |
|
|
47
|
+
| `fipsign_sign` | Sign any payload | 1 token |
|
|
48
|
+
| `fipsign_verify` | Verify a signed token | 1 token |
|
|
49
|
+
| `fipsign_revoke` | Permanently revoke a token | 1 token |
|
|
50
|
+
| `fipsign_usage` | Get token balance and usage history | free |
|
|
51
|
+
| `fipsign_generate_key_pair` | Generate an ML-DSA-65 key pair locally | free |
|
|
52
|
+
| `fipsign_ca_issue` | Issue a post-quantum certificate | 1 token |
|
|
53
|
+
| `fipsign_ca_revoke_cert` | Revoke a certificate | 1 token |
|
|
54
|
+
| `fipsign_ca_get_cert` | Get certificate status by ID | free |
|
|
55
|
+
| `fipsign_ca_get_crl` | Get the Certificate Revocation List | free |
|
|
56
|
+
| `fipsign_webhooks_register` | Register a webhook endpoint | free |
|
|
57
|
+
| `fipsign_webhooks_get` | Get current webhook config | free |
|
|
58
|
+
| `fipsign_webhooks_delete` | Delete webhook configuration | free |
|
|
59
|
+
| `fipsign_webhooks_test` | Send a test event to your webhook | free |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Prerequisites
|
|
64
|
+
|
|
65
|
+
1. Python 3.10 or later
|
|
66
|
+
2. A FIPSign account and API key — [create one free at app.fipsign.dev](https://app.fipsign.dev)
|
|
67
|
+
3. For CA tools: a CA created inside your project from the dashboard
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Local testing before publishing
|
|
72
|
+
|
|
73
|
+
### Level 1 — MCP Inspector (no Claude Desktop required)
|
|
74
|
+
|
|
75
|
+
The Inspector opens a browser UI where you can call each tool manually and inspect responses without Claude Desktop.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/fipsign/fipsign-mcp-python
|
|
79
|
+
cd fipsign-mcp-python
|
|
80
|
+
pip install -e .
|
|
81
|
+
export FIPSIGN_API_KEY=pqa_your_real_key
|
|
82
|
+
npx @modelcontextprotocol/inspector python -m fipsign_mcp.server
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Open the URL shown in the terminal (typically `http://localhost:5173`). Select a tool, fill in the parameters, and run it.
|
|
86
|
+
|
|
87
|
+
### Level 2 — Claude Desktop with local code (without publishing to PyPI)
|
|
88
|
+
|
|
89
|
+
Install in editable mode, then point Claude Desktop at the module:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install -e .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Add to your `claude_desktop_config.json` (see path below):
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"fipsign": {
|
|
101
|
+
"command": "python",
|
|
102
|
+
"args": ["-m", "fipsign_mcp.server"],
|
|
103
|
+
"env": {
|
|
104
|
+
"FIPSIGN_API_KEY": "pqa_your_real_key"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Level 3 — Claude Desktop with published package (production)
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"mcpServers": {
|
|
116
|
+
"fipsign": {
|
|
117
|
+
"command": "uvx",
|
|
118
|
+
"args": ["fipsign-mcp"],
|
|
119
|
+
"env": {
|
|
120
|
+
"FIPSIGN_API_KEY": "pqa_your_real_key"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or with pip-installed package:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"fipsign": {
|
|
133
|
+
"command": "fipsign-mcp",
|
|
134
|
+
"env": {
|
|
135
|
+
"FIPSIGN_API_KEY": "pqa_your_real_key"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Installation for Claude Desktop
|
|
145
|
+
|
|
146
|
+
`claude_desktop_config.json` is located at:
|
|
147
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
148
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
149
|
+
- **Linux:** `~/.config/Claude/claude_desktop_config.json`
|
|
150
|
+
|
|
151
|
+
Add the `fipsign` entry inside `mcpServers` (create the file if it doesn't exist):
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"mcpServers": {
|
|
156
|
+
"fipsign": {
|
|
157
|
+
"command": "uvx",
|
|
158
|
+
"args": ["fipsign-mcp"],
|
|
159
|
+
"env": {
|
|
160
|
+
"FIPSIGN_API_KEY": "pqa_your_real_key"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Restart Claude Desktop after editing the config.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Installation for Claude Code
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
claude mcp add fipsign -- env FIPSIGN_API_KEY=pqa_your_real_key uvx fipsign-mcp
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Or manually in your project's `.claude/mcp.json`:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"mcpServers": {
|
|
182
|
+
"fipsign": {
|
|
183
|
+
"command": "uvx",
|
|
184
|
+
"args": ["fipsign-mcp"],
|
|
185
|
+
"env": {
|
|
186
|
+
"FIPSIGN_API_KEY": "pqa_your_real_key"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Environment variables
|
|
196
|
+
|
|
197
|
+
| Variable | Required | Default | Description |
|
|
198
|
+
|---|---|---|---|
|
|
199
|
+
| `FIPSIGN_API_KEY` | Yes (for most tools) | — | Your FIPSign API key. Format: `pqa_` + 64 lowercase hex chars. Get one at app.fipsign.dev. |
|
|
200
|
+
| `FIPSIGN_BASE_URL` | No | `https://api.fipsign.dev` | Override API base URL (useful for self-hosted instances or local dev). |
|
|
201
|
+
|
|
202
|
+
`fipsign_health`, `fipsign_public_key`, and `fipsign_generate_key_pair` work without an API key.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Key pair generation — Python vs JS SDK note
|
|
207
|
+
|
|
208
|
+
`fipsign_generate_key_pair` returns the `secretKey` as the **32-byte ML-DSA-65 seed** (base64), not the 4032-byte expanded key returned by the JS SDK's `generateKeyPair()`. The `publicKey` (1952 bytes) is identical in both SDKs and fully compatible with `fipsign_ca_issue`.
|
|
209
|
+
|
|
210
|
+
This difference only matters if you need to sign data locally on a Python device using the returned `secretKey`:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA65PrivateKey
|
|
214
|
+
import base64
|
|
215
|
+
|
|
216
|
+
private_key = MLDSA65PrivateKey.from_seed_bytes(base64.b64decode(secret_key))
|
|
217
|
+
signature = private_key.sign(message)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Usage examples
|
|
223
|
+
|
|
224
|
+
Once configured, you can ask Claude:
|
|
225
|
+
|
|
226
|
+
**Signing:**
|
|
227
|
+
- *"Sign a token for user_123 with role admin that expires in 1 hour"*
|
|
228
|
+
- *"Verify this token: { payload: '...', signature: '...', algorithm: 'ML-DSA-65', issuedAt: 123 }"*
|
|
229
|
+
- *"Revoke this token because the user logged out"*
|
|
230
|
+
|
|
231
|
+
**Certificates:**
|
|
232
|
+
- *"Generate a key pair for a new IoT device"*
|
|
233
|
+
- *"Issue a certificate for device-serial-00123 using the public key I just generated, valid for 1 year"*
|
|
234
|
+
- *"Check the revocation status of cert_abc123"*
|
|
235
|
+
- *"Get the full CRL for our CA"*
|
|
236
|
+
- *"Revoke certificate cert_abc123 — device was reported stolen"*
|
|
237
|
+
|
|
238
|
+
**Monitoring:**
|
|
239
|
+
- *"How many tokens do I have left this month?"*
|
|
240
|
+
- *"Register a webhook at https://myapp.com/hooks/fipsign for limit.warning and limit.reached events"*
|
|
241
|
+
- *"Send a test event to my webhook"*
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Publishing to PyPI
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
pip install build twine
|
|
249
|
+
python -m build
|
|
250
|
+
twine upload dist/*
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Links
|
|
256
|
+
|
|
257
|
+
- Dashboard: [app.fipsign.dev](https://app.fipsign.dev)
|
|
258
|
+
- API status: [status.fipsign.dev](https://status.fipsign.dev)
|
|
259
|
+
- JS SDK: [npmjs.com/package/fipsign-sdk](https://www.npmjs.com/package/fipsign-sdk)
|
|
260
|
+
- Python SDK: [pypi.org/project/fipsign-sdk](https://pypi.org/project/fipsign-sdk/)
|
|
261
|
+
- TypeScript MCP: [npmjs.com/package/@fipsign/mcp](https://www.npmjs.com/package/@fipsign/mcp)
|
|
262
|
+
- NIST FIPS 204: [csrc.nist.gov/pubs/fips/204/final](https://csrc.nist.gov/pubs/fips/204/final)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
fipsign_mcp/__init__.py,sha256=59OAldt0Vis4pgb3zp87Yab8wfwCWrtos14WLDDx_Yc,191
|
|
2
|
+
fipsign_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
fipsign_mcp/server.py,sha256=Gx3J4g_H7L9Uoh2YIhJpHZkXYhu79cmS6AazQo3n4GE,29377
|
|
4
|
+
fipsign_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=aNlBaMDkGDQQtmh1FVbJgnvhVGq-UMGhtfDzzi-6XCM,1064
|
|
5
|
+
fipsign_mcp-0.1.0.dist-info/METADATA,sha256=qJBe4aOJPrsemaDMWyjzh8aqT48hDDWHrvmu-GqljwE,8031
|
|
6
|
+
fipsign_mcp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
fipsign_mcp-0.1.0.dist-info/entry_points.txt,sha256=YjTK90VHYOh4MFuXdQDtgn4bkPgfz0MRnilQSWDT2Yk,56
|
|
8
|
+
fipsign_mcp-0.1.0.dist-info/top_level.txt,sha256=sNpdyHpiTYVkFcHsdseBNo0BPXsX6Sk0_ACOEJl3c_0,12
|
|
9
|
+
fipsign_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FIPSign
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fipsign_mcp
|