Skip to content
devtoolbox.cc

jwt_sign MCP tool: create signed test JWTs from an AI agent

· 6 min read

Get the devtoolbox.cc MCP serverOpen the JWT Debugger tool

Tests for an authenticated API need tokens: an admin token, an expired one, a token for the wrong audience, a token signed with the wrong key. Writing these by hand means Base64URL-encoding JSON, computing an HMAC or an RSA signature, and joining the parts with dots. An assistant can write a script that does it, but it can’t produce a valid signature by typing. The signature has to be computed.

The devtoolbox.cc MCP server has a tool for this, jwt_sign. The assistant passes the header and claims as JSON, the tool signs them with your key and returns the token. It uses the same code as the encoder in the JWT debugger on this site. Installation is on the MCP page.

What the tool does

  • Signs with HS256, HS384 and HS512 (shared secret), or RS, PS and ES algorithms (private key as PKCS#8 PEM or JWK).
  • Takes the header and payload as JSON text, so claims come out exactly as written, including large numeric IDs.
  • Uses {"alg":"HS256","typ":"JWT"} as the header if you don’t give one.
  • Refuses to create unsigned tokens (alg: none).

HMAC signatures, and RSA signatures with RS256, RS384 or RS512, are deterministic: the same header, payload and key always give the same token. PS and ES signatures contain randomness, so they differ on every call and are all valid.

Arguments

Argument Type Meaning
payload string The claims as JSON. Required.
header string The header as JSON. Must name the alg. Default HS256 with typ.
key string Secret, PEM or JWK, inline. Use this only for throwaway test keys.
key_path string A file with the key. A trailing newline in a secret file is ignored.
key_env string The name of an environment variable holding the key.
base64_secret boolean The HMAC secret is Base64-encoded. Default false.

Prefer key_path or key_env. The tool then reads the key itself and the secret never passes through the conversation.

Examples

These were run against the real tool with JWT_SECRET=your-256-bit-secret in the server’s environment. The claims of the well-known example token from jwt.io, with the default header:

{
  "payload": "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}",
  "key_env": "JWT_SECRET"
}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

That’s exactly the token jwt.io shows, character for character, because HS256 is deterministic. A test token with HS512, a key ID in the header and an expiry:

{
  "header": "{\"alg\":\"HS512\",\"typ\":\"JWT\",\"kid\":\"test-2026\"}",
  "payload": "{\"sub\":\"user-42\",\"aud\":\"api.example.com\",\"role\":\"admin\",\"exp\":1798761600}",
  "key_env": "JWT_SECRET"
}
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtMjAyNiJ9.eyJzdWIiOiJ1c2VyLTQyIiwiYXVkIjoiYXBpLmV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzk4NzYxNjAwfQ.BN_J7DfDr7TDrbjnjxsocSllmJ8PB38kcUxJp24oeWXHoh_LbUNpnrnnKA_1ET6uNmz36l_bhusb2wUlTZHc4g

The exp of 1798761600 is 2027-01-01 00:00 UTC. An assistant can get such values from the timestamp_convert tool instead of doing the arithmetic. Numbers are written exactly as given, which matters for IDs that don’t fit in a double:

{ "payload": "{\"sub\":\"svc\",\"tenant\":9007199254740993}", "key_env": "JWT_SECRET" }
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdmMiLCJ0ZW5hbnQiOjkwMDcxOTkyNTQ3NDA5OTN9.ovkSFrzj3b0rRzOL4oRfBI7Zs5Dx0oWJK6rlT1AAv10

Unsigned tokens are refused, since there’s no legitimate reason for a test suite to depend on them:

{ "header": "{\"alg\":\"none\"}", "payload": "{\"sub\":\"admin\"}", "key": "x" }
Refusing to create an unsigned (alg "none") token

A key that doesn’t fit the algorithm fails instead of producing a broken token. Here an RS256 header meets an HMAC secret, and the message comes from the signing library, which expected an RSA private key in PKCS#8 PEM format:

{ "header": "{\"alg\":\"RS256\"}", "payload": "{\"sub\":\"x\"}", "key_env": "JWT_SECRET" }
"pkcs8" must be PKCS#8 formatted string

For RS256, store the private key in a PEM file and pass key_path. Services verify those tokens with the matching public key, which jwt_decode_verify accepts as PEM, X.509 certificate or JWK.

Building a set of test tokens

An agent writing authentication tests can produce the whole matrix in one go:

  • a valid token for each role your API knows;
  • a token whose exp is in the past, and one whose nbf is in the future;
  • a token for the wrong aud and one from the wrong iss;
  • a token signed with a different secret, to check that verification really happens.

Each of those is one jwt_sign call with different claims or a different key. Saved as fixtures, they make tests independent of a running identity provider. Use dedicated test keys for this. Tokens signed with a production key are real credentials, wherever they end up.

Choosing exp, nbf and iat

The time claims are NumericDates: seconds since 1970, not milliseconds. A value with 13 digits is the most common mistake in hand-written tokens, and it produces a token that expires in the year 57,000. Some rules that keep test tokens realistic:

  • iat is when the token was issued, exp when it expires. Access tokens usually live minutes to hours, not days.
  • For a “not yet valid” test, set nbf in the future.
  • For an “expired” test, set exp in the past, not to zero. Some code treats 0 as “no expiry”.

The assistant can get exact values from the timestamp_convert tool of the same server:

{ "date": "2027-01-01T00:00:00Z" }
Seconds: 1798761600
Milliseconds: 1798761600000
ISO 8601: 2027-01-01T00:00:00.000Z
RFC 2822: Fri, 01 Jan 2027 00:00:00 +0000
UTC (+00:00): 2027-01-01 00:00:00
Relative: in 2 months

The seconds value is the exp used in the HS512 example above.

Keep the generated tokens next to the tests that use them, and regenerate them whenever the test signing key changes.

Things to ask your assistant

  • “Create an HS256 token for user-42 with role viewer, expiring in one hour, signed with JWT_SECRET.”
  • “Generate expired, not-yet-valid and wrong-audience tokens for the tests in auth.spec.ts, signed with keys/test-private.pem.”
  • “Re-sign the payload of token.txt with exp moved to next week.”

To build a token interactively, the encoder in the JWT debugger shows header, payload and the signed result side by side. The Unix timestamp converter helps with exp, nbf and iat values.

Install

jwt_sign is part of the devtoolbox.cc MCP server, which runs locally and makes no network requests. Keys and tokens stay on your computer. Follow the steps for Claude Desktop, Claude Code, Cursor or VS Code on the MCP install page.

← All posts