chron-mcp 0.1.0

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/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # Chron
2
+
3
+ > A timestamped audit trail for every AI conversation — stored locally, owned by you.
4
+
5
+ AI tools show when you sent a message. Chron logs when the AI responded too — and keeps a permanent, queryable record of every exchange across every tool you use.
6
+
7
+ Works with Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP-compatible AI tool.
8
+
9
+ ---
10
+
11
+ ## Why
12
+
13
+ AI tools produce no audit trail by default. You cannot answer:
14
+ - What did the AI say, and when exactly?
15
+ - How long did the AI take to respond?
16
+ - What was the full conversation that produced this output?
17
+ - What did I ask Claude last week about this codebase?
18
+
19
+ Chron fixes that. Every exchange is logged with a precise local datetime (including timezone offset) to a SQLite file you own. No cloud, no vendor lock-in, no data leaving your machine.
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ Add to your AI tool's MCP config:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "chron": {
31
+ "command": "npx",
32
+ "args": ["-y", "chron-mcp"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ First run creates `~/.chron/chron.db` automatically. No database setup, no env vars, no migrations.
39
+
40
+ ---
41
+
42
+ ## What it logs
43
+
44
+ Every exchange is recorded with precise local timestamps — user message when received, assistant response when sent:
45
+
46
+ ```
47
+ [user: 2026-05-08 14:32:11 +02:00 | assistant: 2026-05-08 14:32:43 +02:00]
48
+
49
+ The main risks of deploying this contract are...
50
+ ```
51
+
52
+ The gap between user and assistant timestamps is real generation time. Both are stored in your local SQLite DB with full timezone offset.
53
+
54
+ ---
55
+
56
+ ## Config by tool
57
+
58
+ ### Claude Desktop
59
+
60
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "chron": {
66
+ "command": "npx",
67
+ "args": ["-y", "chron-mcp"]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### Claude Code
74
+
75
+ ```bash
76
+ claude mcp add chron npx -y chron-mcp
77
+ ```
78
+
79
+ Then add the skill hook to `~/.claude/settings.json`:
80
+
81
+ ```json
82
+ {
83
+ "hooks": {
84
+ "SessionStart": [
85
+ {
86
+ "hooks": [
87
+ {
88
+ "type": "command",
89
+ "command": "cat ~/.chron/chron.skill.md"
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Cursor
99
+
100
+ Edit `~/.cursor/mcp.json`:
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "chron": {
106
+ "command": "npx",
107
+ "args": ["-y", "chron-mcp"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### Windsurf
114
+
115
+ Edit `~/.codeium/windsurf/mcp_config.json`:
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "chron": {
121
+ "command": "npx",
122
+ "args": ["-y", "chron-mcp"]
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Companion skill file
131
+
132
+ Chron ships with `skills/chron.skill.md` — a plain-text instruction file that tells the AI how to use the MCP tools automatically. Load it into your AI tool once. After that, the AI:
133
+
134
+ 1. Creates or resumes a named session at the start of every conversation
135
+ 2. Logs your message before it starts responding (captures the real user timestamp)
136
+ 3. Logs its response after composing it (captures the real assistant timestamp)
137
+ 4. Shows `[user: YYYY-MM-DD HH:MM:SS ±HH:MM | assistant: YYYY-MM-DD HH:MM:SS ±HH:MM]` at the top of every response
138
+ 5. Retrieves prior session history so context is never lost across conversations
139
+
140
+ ---
141
+
142
+ ## MCP Tools
143
+
144
+ | Tool | Description |
145
+ |---|---|
146
+ | `start_session` | Create or resume a named audit session |
147
+ | `log_message` | Record a single message with the current local datetime |
148
+ | `log_exchange` | Log a user/assistant pair atomically (for batch imports) |
149
+ | `list_sessions` | List all sessions ordered by most recently active |
150
+ | `get_session_history` | Retrieve the full timestamped log for a session |
151
+
152
+ ---
153
+
154
+ ## Configuration
155
+
156
+ | Env var | Default | Description |
157
+ |---|---|---|
158
+ | `CHRON_DB_PATH` | `~/.chron/chron.db` | Path to SQLite database file |
159
+ | `CHRON_TRANSPORT` | `stdio` | Set to `http` to enable HTTP+SSE mode |
160
+ | `CHRON_API_KEY` | _(none)_ | Bearer token for HTTP mode |
161
+ | `PORT` | `3001` | Port for HTTP mode |
162
+
163
+ ---
164
+
165
+ ## HTTP+SSE mode (team / self-hosted)
166
+
167
+ For teams or remote use, run Chron as an HTTP server:
168
+
169
+ ```bash
170
+ CHRON_TRANSPORT=http CHRON_API_KEY=your-key PORT=3001 npx chron-mcp
171
+ ```
172
+
173
+ Point your MCP config at the URL:
174
+
175
+ ```json
176
+ {
177
+ "mcpServers": {
178
+ "chron": {
179
+ "url": "https://your-server/mcp",
180
+ "headers": {
181
+ "Authorization": "Bearer your-key"
182
+ }
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Your data
191
+
192
+ Your audit log lives at `~/.chron/chron.db` — a single SQLite file on your machine. Query it directly with any SQLite tool:
193
+
194
+ ```bash
195
+ sqlite3 ~/.chron/chron.db \
196
+ "SELECT s.title, m.role, m.content, m.created_at
197
+ FROM messages m JOIN sessions s ON s.id = m.session_id
198
+ ORDER BY m.created_at"
199
+ ```
200
+
201
+ No cloud, no telemetry, no data leaving your machine. Change the location with `CHRON_DB_PATH`.
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ MIT
@@ -0,0 +1,3 @@
1
+ import * as schema from './schema';
2
+ export declare function initDb(dbPath?: string): Promise<import("drizzle-orm/libsql").LibSQLDatabase<typeof schema>>;
3
+ export type Db = Awaited<ReturnType<typeof initDb>>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.initDb = initDb;
37
+ const client_1 = require("@libsql/client");
38
+ const libsql_1 = require("drizzle-orm/libsql");
39
+ const os_1 = require("os");
40
+ const fs_1 = require("fs");
41
+ const path_1 = require("path");
42
+ const schema = __importStar(require("./schema"));
43
+ function getDbPath() {
44
+ const envPath = process.env.CHRON_DB_PATH;
45
+ if (envPath)
46
+ return envPath;
47
+ return (0, path_1.join)((0, os_1.homedir)(), '.chron', 'chron.db');
48
+ }
49
+ const CREATE_SQL = [
50
+ `CREATE TABLE IF NOT EXISTS sessions (
51
+ id TEXT PRIMARY KEY,
52
+ title TEXT NOT NULL,
53
+ ai_tool TEXT,
54
+ created_at TEXT NOT NULL,
55
+ updated_at TEXT NOT NULL
56
+ )`,
57
+ `CREATE TABLE IF NOT EXISTS messages (
58
+ id TEXT PRIMARY KEY,
59
+ session_id TEXT NOT NULL,
60
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
61
+ content TEXT NOT NULL,
62
+ created_at TEXT NOT NULL,
63
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
64
+ )`,
65
+ ];
66
+ async function initDb(dbPath) {
67
+ const path = dbPath ?? getDbPath();
68
+ if (!path.startsWith(':')) {
69
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
70
+ }
71
+ const client = (0, client_1.createClient)({ url: path.startsWith(':') ? path : `file:${path}` });
72
+ await client.execute('PRAGMA journal_mode = WAL');
73
+ await client.execute('PRAGMA foreign_keys = ON');
74
+ for (const sql of CREATE_SQL) {
75
+ await client.execute(sql);
76
+ }
77
+ return (0, libsql_1.drizzle)(client, { schema });
78
+ }
@@ -0,0 +1,134 @@
1
+ export declare const sessions: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
2
+ name: "sessions";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6
+ name: "id";
7
+ tableName: "sessions";
8
+ dataType: "string";
9
+ columnType: "SQLiteText";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: false;
14
+ enumValues: [string, ...string[]];
15
+ baseColumn: never;
16
+ }, object>;
17
+ title: import("drizzle-orm/sqlite-core").SQLiteColumn<{
18
+ name: "title";
19
+ tableName: "sessions";
20
+ dataType: "string";
21
+ columnType: "SQLiteText";
22
+ data: string;
23
+ driverParam: string;
24
+ notNull: true;
25
+ hasDefault: false;
26
+ enumValues: [string, ...string[]];
27
+ baseColumn: never;
28
+ }, object>;
29
+ ai_tool: import("drizzle-orm/sqlite-core").SQLiteColumn<{
30
+ name: "ai_tool";
31
+ tableName: "sessions";
32
+ dataType: "string";
33
+ columnType: "SQLiteText";
34
+ data: string;
35
+ driverParam: string;
36
+ notNull: false;
37
+ hasDefault: false;
38
+ enumValues: [string, ...string[]];
39
+ baseColumn: never;
40
+ }, object>;
41
+ created_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
42
+ name: "created_at";
43
+ tableName: "sessions";
44
+ dataType: "string";
45
+ columnType: "SQLiteText";
46
+ data: string;
47
+ driverParam: string;
48
+ notNull: true;
49
+ hasDefault: false;
50
+ enumValues: [string, ...string[]];
51
+ baseColumn: never;
52
+ }, object>;
53
+ updated_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
54
+ name: "updated_at";
55
+ tableName: "sessions";
56
+ dataType: "string";
57
+ columnType: "SQLiteText";
58
+ data: string;
59
+ driverParam: string;
60
+ notNull: true;
61
+ hasDefault: false;
62
+ enumValues: [string, ...string[]];
63
+ baseColumn: never;
64
+ }, object>;
65
+ };
66
+ dialect: "sqlite";
67
+ }>;
68
+ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
69
+ name: "messages";
70
+ schema: undefined;
71
+ columns: {
72
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
73
+ name: "id";
74
+ tableName: "messages";
75
+ dataType: "string";
76
+ columnType: "SQLiteText";
77
+ data: string;
78
+ driverParam: string;
79
+ notNull: true;
80
+ hasDefault: false;
81
+ enumValues: [string, ...string[]];
82
+ baseColumn: never;
83
+ }, object>;
84
+ session_id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
85
+ name: "session_id";
86
+ tableName: "messages";
87
+ dataType: "string";
88
+ columnType: "SQLiteText";
89
+ data: string;
90
+ driverParam: string;
91
+ notNull: true;
92
+ hasDefault: false;
93
+ enumValues: [string, ...string[]];
94
+ baseColumn: never;
95
+ }, object>;
96
+ role: import("drizzle-orm/sqlite-core").SQLiteColumn<{
97
+ name: "role";
98
+ tableName: "messages";
99
+ dataType: "string";
100
+ columnType: "SQLiteText";
101
+ data: "user" | "assistant";
102
+ driverParam: string;
103
+ notNull: true;
104
+ hasDefault: false;
105
+ enumValues: ["user", "assistant"];
106
+ baseColumn: never;
107
+ }, object>;
108
+ content: import("drizzle-orm/sqlite-core").SQLiteColumn<{
109
+ name: "content";
110
+ tableName: "messages";
111
+ dataType: "string";
112
+ columnType: "SQLiteText";
113
+ data: string;
114
+ driverParam: string;
115
+ notNull: true;
116
+ hasDefault: false;
117
+ enumValues: [string, ...string[]];
118
+ baseColumn: never;
119
+ }, object>;
120
+ created_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
121
+ name: "created_at";
122
+ tableName: "messages";
123
+ dataType: "string";
124
+ columnType: "SQLiteText";
125
+ data: string;
126
+ driverParam: string;
127
+ notNull: true;
128
+ hasDefault: false;
129
+ enumValues: [string, ...string[]];
130
+ baseColumn: never;
131
+ }, object>;
132
+ };
133
+ dialect: "sqlite";
134
+ }>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messages = exports.sessions = void 0;
4
+ const sqlite_core_1 = require("drizzle-orm/sqlite-core");
5
+ exports.sessions = (0, sqlite_core_1.sqliteTable)('sessions', {
6
+ id: (0, sqlite_core_1.text)('id').primaryKey(),
7
+ title: (0, sqlite_core_1.text)('title').notNull(),
8
+ ai_tool: (0, sqlite_core_1.text)('ai_tool'),
9
+ created_at: (0, sqlite_core_1.text)('created_at').notNull(),
10
+ updated_at: (0, sqlite_core_1.text)('updated_at').notNull(),
11
+ });
12
+ exports.messages = (0, sqlite_core_1.sqliteTable)('messages', {
13
+ id: (0, sqlite_core_1.text)('id').primaryKey(),
14
+ session_id: (0, sqlite_core_1.text)('session_id').notNull(),
15
+ role: (0, sqlite_core_1.text)('role', { enum: ['user', 'assistant'] }).notNull(),
16
+ content: (0, sqlite_core_1.text)('content').notNull(),
17
+ created_at: (0, sqlite_core_1.text)('created_at').notNull(),
18
+ });
@@ -0,0 +1,2 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ export declare function apiKeyMiddleware(apiKey: string): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiKeyMiddleware = apiKeyMiddleware;
4
+ function apiKeyMiddleware(apiKey) {
5
+ return (req, res, next) => {
6
+ const header = req.headers.authorization;
7
+ if (!header || header !== `Bearer ${apiKey}`) {
8
+ res.status(401).end();
9
+ return;
10
+ }
11
+ next();
12
+ };
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { Db } from '../db/index';
3
+ export declare function startHttpServer(server: McpServer, db: Db): Promise<void>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startHttpServer = startHttpServer;
7
+ const express_1 = __importDefault(require("express"));
8
+ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
9
+ const middleware_1 = require("./middleware");
10
+ const server_1 = require("../server");
11
+ async function startHttpServer(server, db) {
12
+ const app = (0, express_1.default)();
13
+ app.use(express_1.default.json());
14
+ const apiKey = process.env.CHRON_API_KEY;
15
+ if (apiKey) {
16
+ app.use((0, middleware_1.apiKeyMiddleware)(apiKey));
17
+ }
18
+ const transports = new Map();
19
+ app.get('/sse', async (req, res) => {
20
+ const instanceServer = (0, server_1.createServer)(db);
21
+ const transport = new sse_js_1.SSEServerTransport('/messages', res);
22
+ transports.set(transport.sessionId, { transport, server: instanceServer });
23
+ transport.onclose = () => transports.delete(transport.sessionId);
24
+ await instanceServer.connect(transport);
25
+ });
26
+ app.post('/messages', async (req, res) => {
27
+ const sessionId = req.query.sessionId;
28
+ const entry = transports.get(sessionId);
29
+ if (!entry) {
30
+ res.status(404).json({ error: 'Session not found' });
31
+ return;
32
+ }
33
+ await entry.transport.handlePostMessage(req, res);
34
+ });
35
+ const port = parseInt(process.env.PORT ?? '3001', 10);
36
+ await new Promise(resolve => app.listen(port, resolve));
37
+ process.stderr.write(`Chron HTTP server listening on port ${port}\n`);
38
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
38
+ const index_1 = require("./db/index");
39
+ const server_1 = require("./server");
40
+ async function main() {
41
+ const db = await (0, index_1.initDb)();
42
+ const server = (0, server_1.createServer)(db);
43
+ if (process.env.CHRON_TRANSPORT === 'http') {
44
+ const { startHttpServer } = await Promise.resolve().then(() => __importStar(require('./http/server')));
45
+ await startHttpServer(server, db);
46
+ }
47
+ else {
48
+ const transport = new stdio_js_1.StdioServerTransport();
49
+ await server.connect(transport);
50
+ }
51
+ }
52
+ main().catch((err) => {
53
+ process.stderr.write(`Failed to start chron: ${err}\n`);
54
+ process.exit(1);
55
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { Db } from './db/index';
3
+ export declare function createServer(db: Db): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createServer = createServer;
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const zod_1 = require("zod");
6
+ const sessions_1 = require("./tools/sessions");
7
+ const messages_1 = require("./tools/messages");
8
+ const roleEnum = zod_1.z.enum(['user', 'assistant']);
9
+ function createServer(db) {
10
+ const server = new mcp_js_1.McpServer({
11
+ name: 'chron',
12
+ version: '0.1.0',
13
+ });
14
+ // @ts-expect-error — MCP SDK deep generic type instantiation limit hit; runtime types are correct
15
+ server.tool('start_session', 'Create a new audit session or resume an existing one by title. Call this at the start of every conversation.', {
16
+ title: zod_1.z.string().describe('Descriptive session title, e.g. "Contract review — 2026-05-08"'),
17
+ ai_tool: zod_1.z.string().optional().describe('AI tool name: "claude", "cursor", "windsurf", etc.'),
18
+ }, (0, sessions_1.startSession)(db));
19
+ server.tool('log_message', 'Record a single message (user or assistant) with the current UTC datetime. Call before responding for user messages, and before sending for assistant messages.', {
20
+ session_id: zod_1.z.string().describe('Session ID returned by start_session'),
21
+ role: roleEnum,
22
+ content: zod_1.z.string().describe('Full message text'),
23
+ }, (0, messages_1.logMessage)(db));
24
+ server.tool('log_exchange', 'Log a user message and assistant response together in one call. Preferred over two log_message calls — faster and atomic.', {
25
+ session_id: zod_1.z.string().describe('Session ID returned by start_session'),
26
+ user_content: zod_1.z.string().describe('The exact user message'),
27
+ assistant_content: zod_1.z.string().describe('The exact assistant response'),
28
+ }, (0, messages_1.logExchange)(db));
29
+ // @ts-expect-error — MCP SDK deep generic type instantiation limit hit; runtime types are correct
30
+ server.tool('list_sessions', 'List all audit sessions ordered by most recently active. Returns id, title, ai_tool, message_count, created_at, updated_at.', {
31
+ limit: zod_1.z.number().int().positive().optional().describe('Return only the most recent N sessions'),
32
+ }, (0, sessions_1.listSessions)(db));
33
+ server.tool('get_session_history', 'Retrieve the full timestamped audit log for a session, oldest first.', {
34
+ session_id: zod_1.z.string(),
35
+ limit: zod_1.z.number().int().positive().optional().describe('Return only the most recent N messages'),
36
+ }, (0, sessions_1.getSessionHistory)(db));
37
+ return server;
38
+ }
@@ -0,0 +1,21 @@
1
+ import type { Db } from '../db/index';
2
+ export declare function logMessage(db: Db): (args: {
3
+ session_id: string;
4
+ role: "user" | "assistant";
5
+ content: string;
6
+ }) => Promise<{
7
+ content: {
8
+ type: "text";
9
+ text: string;
10
+ }[];
11
+ }>;
12
+ export declare function logExchange(db: Db): (args: {
13
+ session_id: string;
14
+ user_content: string;
15
+ assistant_content: string;
16
+ }) => Promise<{
17
+ content: {
18
+ type: "text";
19
+ text: string;
20
+ }[];
21
+ }>;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logMessage = logMessage;
4
+ exports.logExchange = logExchange;
5
+ const drizzle_orm_1 = require("drizzle-orm");
6
+ const uuid_1 = require("uuid");
7
+ const schema_1 = require("../db/schema");
8
+ const time_1 = require("../utils/time");
9
+ function logMessage(db) {
10
+ return async (args) => {
11
+ const now = (0, time_1.localISOString)();
12
+ const id = (0, uuid_1.v4)();
13
+ await db.insert(schema_1.messages).values({
14
+ id,
15
+ session_id: args.session_id,
16
+ role: args.role,
17
+ content: args.content,
18
+ created_at: now,
19
+ });
20
+ await db.update(schema_1.sessions)
21
+ .set({ updated_at: now })
22
+ .where((0, drizzle_orm_1.eq)(schema_1.sessions.id, args.session_id));
23
+ return {
24
+ content: [{ type: 'text', text: JSON.stringify({ message_id: id, created_at: now }) }],
25
+ };
26
+ };
27
+ }
28
+ function logExchange(db) {
29
+ return async (args) => {
30
+ const userNow = (0, time_1.localISOString)();
31
+ const userId = (0, uuid_1.v4)();
32
+ const assistantNow = (0, time_1.localISOString)();
33
+ const assistantId = (0, uuid_1.v4)();
34
+ await db.insert(schema_1.messages).values({
35
+ id: userId,
36
+ session_id: args.session_id,
37
+ role: 'user',
38
+ content: args.user_content,
39
+ created_at: userNow,
40
+ });
41
+ await db.insert(schema_1.messages).values({
42
+ id: assistantId,
43
+ session_id: args.session_id,
44
+ role: 'assistant',
45
+ content: args.assistant_content,
46
+ created_at: assistantNow,
47
+ });
48
+ await db.update(schema_1.sessions)
49
+ .set({ updated_at: assistantNow })
50
+ .where((0, drizzle_orm_1.eq)(schema_1.sessions.id, args.session_id));
51
+ return {
52
+ content: [{
53
+ type: 'text',
54
+ text: JSON.stringify({
55
+ user_message_id: userId,
56
+ user_created_at: userNow,
57
+ assistant_message_id: assistantId,
58
+ assistant_created_at: assistantNow,
59
+ }),
60
+ }],
61
+ };
62
+ };
63
+ }
@@ -0,0 +1,27 @@
1
+ import type { Db } from '../db/index';
2
+ export declare function startSession(db: Db): (args: {
3
+ title: string;
4
+ ai_tool?: string;
5
+ }) => Promise<{
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ }>;
11
+ export declare function listSessions(db: Db): (args: {
12
+ limit?: number;
13
+ }) => Promise<{
14
+ content: {
15
+ type: "text";
16
+ text: string;
17
+ }[];
18
+ }>;
19
+ export declare function getSessionHistory(db: Db): (args: {
20
+ session_id: string;
21
+ limit?: number;
22
+ }) => Promise<{
23
+ content: {
24
+ type: "text";
25
+ text: string;
26
+ }[];
27
+ }>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startSession = startSession;
4
+ exports.listSessions = listSessions;
5
+ exports.getSessionHistory = getSessionHistory;
6
+ const drizzle_orm_1 = require("drizzle-orm");
7
+ const uuid_1 = require("uuid");
8
+ const schema_1 = require("../db/schema");
9
+ const time_1 = require("../utils/time");
10
+ function startSession(db) {
11
+ return async (args) => {
12
+ const existing = await db.select().from(schema_1.sessions).where((0, drizzle_orm_1.eq)(schema_1.sessions.title, args.title));
13
+ if (existing.length > 0) {
14
+ const session = existing[0];
15
+ const now = (0, time_1.localISOString)();
16
+ const allMessages = await db.select().from(schema_1.messages).where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, session.id));
17
+ await db.update(schema_1.sessions).set({ updated_at: now }).where((0, drizzle_orm_1.eq)(schema_1.sessions.id, session.id));
18
+ return {
19
+ content: [{
20
+ type: 'text',
21
+ text: JSON.stringify({ session_id: session.id, created: false, message_count: allMessages.length, ai_tool: session.ai_tool }),
22
+ }],
23
+ };
24
+ }
25
+ const now = (0, time_1.localISOString)();
26
+ const id = (0, uuid_1.v4)();
27
+ await db.insert(schema_1.sessions).values({
28
+ id,
29
+ title: args.title,
30
+ ai_tool: args.ai_tool ?? null,
31
+ created_at: now,
32
+ updated_at: now,
33
+ });
34
+ return {
35
+ content: [{
36
+ type: 'text',
37
+ text: JSON.stringify({ session_id: id, created: true, message_count: 0, ai_tool: args.ai_tool ?? null }),
38
+ }],
39
+ };
40
+ };
41
+ }
42
+ function listSessions(db) {
43
+ return async (args) => {
44
+ const rows = await db.select({
45
+ id: schema_1.sessions.id,
46
+ title: schema_1.sessions.title,
47
+ ai_tool: schema_1.sessions.ai_tool,
48
+ created_at: schema_1.sessions.created_at,
49
+ updated_at: schema_1.sessions.updated_at,
50
+ message_count: (0, drizzle_orm_1.sql) `count(${schema_1.messages.id})`,
51
+ })
52
+ .from(schema_1.sessions)
53
+ .leftJoin(schema_1.messages, (0, drizzle_orm_1.eq)(schema_1.messages.session_id, schema_1.sessions.id))
54
+ .groupBy(schema_1.sessions.id)
55
+ .orderBy((0, drizzle_orm_1.desc)(schema_1.sessions.updated_at));
56
+ const limited = args.limit != null ? rows.slice(0, args.limit) : rows;
57
+ return {
58
+ content: [{
59
+ type: 'text',
60
+ text: JSON.stringify({ sessions: limited, total: rows.length }),
61
+ }],
62
+ };
63
+ };
64
+ }
65
+ function getSessionHistory(db) {
66
+ return async (args) => {
67
+ const all = await db.select().from(schema_1.messages)
68
+ .where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, args.session_id))
69
+ .orderBy((0, drizzle_orm_1.asc)(schema_1.messages.created_at));
70
+ const limited = args.limit != null ? all.slice(-args.limit) : all;
71
+ return {
72
+ content: [{
73
+ type: 'text',
74
+ text: JSON.stringify({
75
+ messages: limited.map(m => ({ role: m.role, content: m.content, created_at: m.created_at })),
76
+ total: all.length,
77
+ }),
78
+ }],
79
+ };
80
+ };
81
+ }
@@ -0,0 +1 @@
1
+ export declare function localISOString(): string;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.localISOString = localISOString;
4
+ function localISOString() {
5
+ const d = new Date();
6
+ const offsetMin = -d.getTimezoneOffset(); // positive for UTC+, negative for UTC-
7
+ const sign = offsetMin >= 0 ? '+' : '-';
8
+ const absMin = Math.abs(offsetMin);
9
+ const hh = String(Math.floor(absMin / 60)).padStart(2, '0');
10
+ const mm = String(absMin % 60).padStart(2, '0');
11
+ const year = d.getFullYear();
12
+ const month = String(d.getMonth() + 1).padStart(2, '0');
13
+ const day = String(d.getDate()).padStart(2, '0');
14
+ const hours = String(d.getHours()).padStart(2, '0');
15
+ const minutes = String(d.getMinutes()).padStart(2, '0');
16
+ const seconds = String(d.getSeconds()).padStart(2, '0');
17
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${hh}:${mm}`;
18
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "chron-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Audit-grade timestamped logs for every AI conversation",
5
+ "main": "dist/index.js",
6
+ "vitest": {
7
+ "include": ["tests/**/*.test.ts"],
8
+ "globals": true
9
+ },
10
+ "bin": {
11
+ "chron-mcp": "dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "skills",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc && chmod +x dist/index.js",
23
+ "dev": "tsc --watch",
24
+ "start": "node dist/index.js",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm test && npm run build"
28
+ },
29
+ "keywords": [
30
+ "mcp",
31
+ "ai",
32
+ "audit",
33
+ "logging",
34
+ "claude",
35
+ "cursor"
36
+ ],
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@libsql/client": "^0.17.3",
40
+ "@modelcontextprotocol/sdk": "^1.12.0",
41
+ "drizzle-orm": "^0.30.10",
42
+ "express": "^4.18.2",
43
+ "uuid": "^9.0.0",
44
+ "zod": "^3.22.4"
45
+ },
46
+ "devDependencies": {
47
+ "@types/express": "^4.17.21",
48
+ "@types/node": "^20.0.0",
49
+ "@types/uuid": "^9.0.0",
50
+ "drizzle-kit": "^0.20.14",
51
+ "typescript": "^5.4.5",
52
+ "vitest": "^1.4.0"
53
+ }
54
+ }
File without changes
@@ -0,0 +1,32 @@
1
+ # Chron — AI Audit Log Skill
2
+
3
+ Chron creates an audit trail of every AI conversation. Every message is logged with a precise local datetime (with timezone offset) to a local SQLite database the user owns.
4
+
5
+ ## Session Start
6
+
7
+ Call `start_session` with a descriptive title (e.g. "Contract review — 2026-05-08") and set `ai_tool` to your tool name (e.g. "claude", "cursor", "windsurf").
8
+
9
+ Call `get_session_history` for the returned `session_id`. If prior messages exist, display:
10
+ > "Resuming session: [N messages] since [first_message_date]"
11
+
12
+ ## Every Exchange
13
+
14
+ 1. Call `log_message(session_id, role="user", content=<exact user message>)` → returns T1
15
+ 2. Compose your response body (plain text only — no timestamp header)
16
+ 3. Call `log_message(session_id, role="assistant", content=<response body>)` → returns T2
17
+ 4. Output header + body:
18
+
19
+ ```
20
+ [user: YYYY-MM-DD HH:MM:SS ±HH:MM | assistant: YYYY-MM-DD HH:MM:SS ±HH:MM]
21
+
22
+ <response body>
23
+ ```
24
+
25
+ T1 and T2 are real server timestamps — T2 − T1 is the actual generation time. The DB stores clean body content without the header.
26
+
27
+ **Do not use `log_exchange` for live conversations** — it captures both timestamps at the same instant and cannot show a real gap. Use it only for batch imports of historical messages.
28
+
29
+ ## If Chron MCP is unavailable
30
+
31
+ State once: "Chron is not connected — this conversation will not be logged."
32
+ Continue normally. Do not retry repeatedly.