Skip to main content
Glama
haggifer

session-clock

by haggifer
README.md
# mcp-server-session-clock

A minimal remote **MCP server** that gives an AI assistant a clock.

Models in a chat or agent host usually have no reliable sense of wall-clock time
— often not even the current time of day — so they can't say when a message
happened or how long ago. This server exposes a single MCP tool, `now`, that
returns the current instant. A [standing instruction](#standing-instruction)
tells the model to call it at the start of each reply to timestamp the exchange,
and again on read-back to work out "how long ago".

It is **stateless** — nothing is stored server-side; the timestamps live in the
conversation itself. It works with any host that accepts remote (Streamable HTTP)
MCP servers.

> ⚠️ **Timestamp capture is best-effort, not guaranteed — read this before relying on it.**
> MCP tool use runs under the host's default `tool_choice: auto`: the model
> decides *per turn* whether to call a tool. It will sometimes skip `now` despite
> the standing instruction — most often on short or trivial messages — so some
> messages won't get a timestamp, and most hosts give you no way to force a call.
> If you need **guaranteed** per-message timestamps, an instruction-driven MCP
> tool (this or any other) is the wrong mechanism — that needs control at the API
> layer (`tool_choice: "any"`), which chat/agent hosts don't expose. This server
> fits "roughly when did things happen", not an audit log.

---

## How it works

1. **Write:** the instruction tells the model to call `now` at the start of each
   reply and not repeat the value in its answer. That call ≈ when your message
   was received.
2. **Storage:** the conversation itself — each `now` call and its result are kept
   in context as a tool-use record, not as prose.
3. **Read:** when you ask "how long ago was X", the model finds the recorded `now`
   result nearest message X and compares it to a fresh `now`.

---

## Prerequisites

- Node 18+ and npm (built with Node 22 / npm 11)
- A Cloudflare account (free tier)
- An MCP host/client that supports **remote (Streamable HTTP) MCP servers**

## Setup

```bash
npm install
```

Copy the local secret template so dev and type generation can see `MCP_SECRET`:

```bash
cp .dev.vars.example .dev.vars
```

Generate TypeScript types (`worker-configuration.d.ts`, git-ignored) — also run
automatically by `npm run typecheck`:

```bash
npm run cf-typegen
```

Generate a secret for the path segment (32+ random chars) and add it to `.dev.vars`:

```bash
node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"
```

Put the printed value in `.dev.vars` as `MCP_SECRET` (replacing the placeholder) —
this is the secret local dev uses. The deployed Worker gets its own copy in the
Deploy step below.

## Run locally

```bash
npm run dev
```

Your endpoint is `http://localhost:8787/mcp/<MCP_SECRET>`. Any other path returns
404.

## Deploy

Install the Wrangler CLI (once per machine):

```bash
npm install -g wrangler
```

Log in to Cloudflare (opens a browser; once per machine):

```bash
wrangler login
```

Publish the Worker:

```bash
npm run deploy
```

Set the secret on the deployed Worker — Wrangler can only attach a secret once the
Worker exists, so this runs *after* the first deploy. Use the same value you put in
`.dev.vars`:

```bash
npx wrangler secret put MCP_SECRET
```

Wrangler prints your public URL. The endpoint is that origin plus the secret path:

```
https://session-clock.<your-subdomain>.workers.dev/mcp/<MCP_SECRET>
```

## Add to your MCP host

1. In your host's MCP-server / connector settings, add a new **remote** server.
2. Paste the full URL above (including `/mcp/<MCP_SECRET>`). Leave auth empty —
   this is an authless server; the secret in the path is the access control.
3. Open a conversation and confirm the `now` tool is available.
4. Add the [standing instruction](#standing-instruction) wherever your host
   injects persistent instructions (system prompt, custom instructions, a
   per-project instruction field, a saved "style", etc.).

## Standing instruction

The server only makes the `now` tool available; this is what makes the model
*use* it. Paste it into your host's persistent-instruction field:

```
I've connected a `now` tool that returns the current time so you can tell me how long ago things happened. This is my own preference and my app shows me every tool call, so nothing here is hidden from me. At the start of each reply, call `now` once to record the time, then answer my message normally — you don't need to repeat the timestamp or describe the call (it's just for later reference), though you can if it's relevant. When I ask how long ago something happened, compare the `now` result nearest that message with a fresh `now` call.
```

- **Word it transparently — this is load-bearing.** An earlier draft said
  *"silently … never mention the tool … reply exactly as you would have without
  it."* Models read that as a request to take **hidden, undisclosed actions** and
  refuse it as a possible prompt injection (they decline to call the tool at all,
  which looks like a random skip but isn't). The version above frames the tool as
  your own, benign, self-disclosed preference and notes the calls are already
  visible to you — **don't reintroduce "silently" / "never mention" phrasing.**
- The timestamp doesn't need to appear in the reply, but your host still shows a
  tool-call indicator for each `now` call, so nothing is actually concealed.
- Times are UTC. "How long ago" is a difference, which is timezone-independent.
- Even well-worded, capture is best-effort — see the note at the top.

## Test it

1. Open a new conversation → send a message; the assistant replies normally (no
   visible timestamp, though your host may show a tool-call indicator).
2. Send a couple more messages over a few minutes.
3. Ask: *"How long ago was my first message?"* — the assistant reads the recorded
   `now` calls and answers within a few minutes' accuracy.

---

## Security model

The tool only reveals the current time and stores nothing, so a leaked URL is
low-stakes (someone could ask what time it is, or try to spam it). Protection is
therefore deliberately light:

- **Secret path segment** (`/mcp/<32+ chars>`) as a de-facto access key; every
  other path 404s. The secret lives in your host's server configuration and in
  Cloudflare's request logs — it's a speed bump against scanners, not real auth.
  Rotate by running `wrangler secret put MCP_SECRET` again and re-pasting the URL.
- **Rate limiting:** add a Cloudflare WAF rate-limit rule on the route so a
  discovered endpoint can't be flooded.

## Limits

- **Not deterministic** (see the note at the top): the model won't call `now` on
  every turn, so some messages have no nearby timestamp and their timing can only
  be inferred from the nearest recorded call.
- **Compaction erases history:** if a long conversation is summarized, early `now`
  results fall out of context and their timing is lost (current time still works).
- **Small tax per reply:** one extra tool round-trip and a tool-call indicator in
  the host UI (the timestamp value is not printed).

## Upgrade (Branch B) — server-side log

If compaction-loss bites, move the log server-side. Two routes:

- **Add storage to this stateless handler:** bind a KV namespace or D1 database,
  persist `(session_token, seq, ts)` keyed by a model-minted token, and add a
  `get_timeline` tool. Least new machinery.
- **Switch to the stateful legacy path** (`createLegacyMcpHandler` / `McpAgent`
  + `WorkerTransport`), which gives each client session its own Durable Object.
  That also lets you test empirically whether an MCP session maps 1:1 to a
  conversation in your host (does a second conversation get a fresh DO?) — if so,
  attribution is automatic with no token needed.

Note that Branch B moves the *storage* server-side; it does **not** make capture
deterministic — the write still depends on the model choosing to call the tool.

---

## Project structure

```
src/index.ts          The Worker: the `now` tool + secret-path gate
src/worker-env.d.ts   Types the MCP_SECRET Worker secret
wrangler.jsonc        Worker config (name, entry point, compatibility)
tsconfig.json         TypeScript config
.dev.vars.example     Template for the local secret
```

Runtime/binding types live in `worker-configuration.d.ts`, generated by
`wrangler types` (git-ignored) — rerun `npm run cf-typegen` after editing
`wrangler.jsonc`, keeping `.dev.vars` present so the secret stays typed.