# Junkt > A hosted key/value store for AI agents, reachable over the Model Context > Protocol (MCP). It gives agents durable state between otherwise stateless > runs — a timestamp, a flag, a counter, or a small JSON blob stored as a > string — without anyone having to run Redis or a database. Junkt is remote and OAuth-gated, not a local stdio server, so it works from hosted clients that cannot run anything on your machine: Claude, Claude Cowork, Claude Code, Claude Desktop, ChatGPT custom connectors, Cursor, and anything else implementing remote MCP. The originating use case is "did this scheduled task already run today?" — an agent that wakes on a schedule and needs to know whether it already did its job, so it neither runs twice nor never runs. ## Endpoint MCP server: https://junkt.io/mcp Authorization: OAuth 2.1 with PKCE and Dynamic Client Registration (RFC 7591). Discovery: /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource ## Tools - list_keys(prefix?: string) -> { keys: [{ key, type, updated_at, expires_at, size }], total } Read-only. Lists keys and metadata, never values. Filter by prefix. - get_value(key: string) -> { exists, key, value, type, created_at, updated_at, expires_at } Read-only. A missing key returns { exists: false, key } rather than an error. - set_value(key: string, value: string, ttl?: integer) -> { key, value, created, updated_at, expires_at } Creates or overwrites a value. ttl is in seconds; omit for a key that never expires. Will not overwrite a counter. - increment(key: string, amount?: integer) -> { key, value, created, updated_at } Atomically adds to a counter, creating it from 0. amount defaults to 1; negative decrements. Use this instead of get/parse/set, which races between concurrent runs. - delete_key(key: string) -> { key, deleted } Idempotent. Deleting a key that does not exist is not an error. - list_controls() -> { controls: [{ key, type, value, options, description, updated_at }], total } Read-only. The settings the account owner has written for the agent to obey. Values come back cast to their type, with the owner's description of what each means. Call it before doing work: the owner may have paused the agent. ## Time A model has no clock, so a date it infers is a guess that nothing downstream can detect as wrong. One tool answers every question about the actual time, with a "kind" naming the calculation. time(kind, timezone?, target?, timestamp?, amount?, unit?) - now — The current date and time, with the calendar date and weekday already worked out. - seconds_until — Whole seconds from now until the next boundary — midnight or the top of the hour — in the given timezone. Pass the answer straight to set_value as a ttl for a key that clears itself at the boundary. Defaults to target midnight. - since — How long has passed since a timestamp you supply. Point it at the updated_at from get_value to answer "how long ago did this last run?". A timestamp in the future returns negative seconds. Requires timestamp. - same_day — Whether a timestamp you supply falls on today's calendar date in the given timezone. This is the "has this already run today?" check, and it is not the same as comparing two ISO strings. Requires timestamp. - add — The date and time a given interval from now, with month lengths and daylight saving handled. Months and years do not overflow, so one month after 31 January is 28 February. A negative amount goes backwards. Requires amount. Defaults to unit days. timezone takes an IANA identifier such as Europe/Tallinn and defaults to UTC; it applies to now, seconds_until, same_day, add. Abbreviations like EST are ambiguous and are rejected. Timestamps are read as ISO 8601 and echoed back parsed. Only your own data is ever required — the timestamp to measure against, the interval to add. Everything else defaults, so "now" and "seconds_until" are complete calls with nothing but a kind. ## Randomness Junkt generates randomness server-side, because a model has no way to produce it: invented values cluster on a few favourites, repeat between runs, and carry far less entropy than they appear to. Each tool takes a "kind" naming the generator to run. random_number(kind, min?, max?, mean?, stddev?, seed?, times?) - int — Random integer between inclusive bounds. Defaults to min 1, max 100. - float — Random float from 0 up to but not including 1. - bool — True or false, 50/50. - percent — Integer from 0 to 100, for probability gates such as "does this fire at 10%?". - normal — Gaussian value with the given mean and standard deviation. Defaults to mean 0, stddev 1. - jitter — Whole seconds from 0 up to max, to spread retries and simultaneous cron wakes. Defaults to max 60. random_token(kind, length?, seed?, times?) - uuid — A valid v4 UUID. - ulid — Time-sortable identifier, so keys carrying it as a suffix list in chronological order. - nanoid — Short URL-safe identifier, length counted in characters. Defaults to length 21. - hex — Random hex string, for lock ownership tokens. Length is counted in bytes, giving twice as many characters. Defaults to length 16. - pin — Numeric code, leading zeros kept, length counted in digits. Defaults to length 6. - slug — Memorable name such as brave-copper-otter, for run IDs and branch names. - passphrase — Diceware-style passphrase, hyphen-separated, length counted in words at roughly 8 bits of entropy each. Defaults to length 4. - color — Random colour as a hex triplet, such as #4f9d8a. - emoji — Random emoji. random_choice(kind, choices, size?, weights?, seed?, times?) - pick — One item chosen from the list, free of the bias in a model choosing for itself. Requires choices. - shuffle — The whole list returned in random order. Requires choices. - sample — Draw "size" items from the list without replacement, so no item is returned twice. Requires choices, size. - weighted — One item chosen in proportion to its weight, for uneven A/B splits and canary rollouts. Weights need not sum to 100. Requires choices, weights. Two arguments apply to every generator. seed makes a draw reproducible — the same seed and arguments always return the same result, for stable bucketing and consistent assignment. The exception is ulid, which is time-based and so can never be reproduced. times returns an array of up to 100 results from a single call. Unseeded draws come from a CSPRNG. A draw cannot be both cryptographically random and reproducible, so a seeded one runs on a deterministic engine keyed by the seed instead. Anything with a sensible default has one, so the shortest useful call is a kind on its own. Only arguments that are your own data — the list to pick from, its weights — are ever required. ## Model - One flat keyspace per account. No buckets, no nesting. - Keys are namespaced by convention, e.g. "cowork:daily-digest:last_run", and list_keys filters by prefix. - A key holds either a value (set_value) or a counter (increment). The type is locked: set_value cannot overwrite a counter, and a value cannot be incremented. - Keys can expire. An expired key is invisible to get_value and list_keys and stops counting toward the key limit. - Alongside its keys, an account has controls: typed settings the owner writes in the dashboard and an agent may only read. Keys are the agent's memory; controls are the owner's instructions to it. A control is a boolean, string, number or enum, carries a description of what it means, and lives under the reserved "control:" namespace — set_value, increment and delete_key all refuse to write there, so an agent cannot clear a control that stops it. Controls have their own allowance and cost nothing against the key limit. - Junkt does not enforce a control. It is somewhere an agent will reliably look, which is worth having and is not the same as a kill switch. - Generated random values are not keys. They are never stored, cannot be read back, and do not count toward the key limit. Pass one to set_value to keep it. - Successful writes, increments and deletes are recorded in a per-account activity log. Reads and generated values are not logged. ## Limits (free tier) - Keys per account: 25 - Controls per account: 10 (a separate allowance) - Value size: 2048 bytes - Key length: 128 characters - Requests: 60 per minute per account - Activity retention: 30 days ## What Junkt is not - Not a database. No queries, relations, indexes or transactions. - Not a vector store or knowledge graph. It remembers that something happened, not what was said. - Not file storage. Values stop at 2048 bytes on purpose; store a reference to anything larger. ## Docs - [Documentation](https://junkt.io/docs): the model, the limits, the keyspace. - [Connect a client](https://junkt.io/docs/connect): setup for Claude, Cowork, Desktop, Code and other MCP clients. - [Tool reference](https://junkt.io/docs/tools): parameters, return shapes and every error message. - [Changelog](https://junkt.io/changelog): what has shipped. - [Pricing](https://junkt.io#pricing): free tier, 25 keys, no card. ## Blog - [MCP has no memory. Here is the smallest thing that fixes it.](https://junkt.io/blog/mcp-has-no-memory): The protocol gives your agent tools, not recollection. Most answers to that reach for embeddings. Most problems need one key and a timestamp. - [Read-modify-write is a race, even when your agent is the only caller](https://junkt.io/blog/read-modify-write-is-a-race): Counting with get and set looks fine until two runs overlap by a few hundred milliseconds. Then it silently undercounts, and nothing anywhere reports an error. - [Why my scheduled agent kept sending the same digest twice](https://junkt.io/blog/why-my-scheduled-agent-kept-sending-the-same-digest-twice): A scheduled Claude task has no idea it has ever run before. That turns out to be a much bigger problem than it sounds, and the fix is one key. ## Contact - Operated by SlashEquip OÜ, a company registered in Estonia. - Support: https://junkt.io/support - Email: support@junkt.io