Imported from sterlingcrispin/stillpoint (
AGENTS.md). Install upstream withnpx skills add sterlingcrispin/stillpoint. Copyright stays with the author.
AGENTS.md
What this project is
Stillpoint is a small Node.js MCP server that returns short text messages from a fixed library. It has four MCP tools, optional HTTP compatibility endpoints, five content categories, and in-memory session tracking. The entire application logic should fit in a few hundred lines of code. Read docs/stillpoint-architecture.md for the full plan. Read docs/stillpoint-v5.md for the design rationale and content specification.
What to build
The deliverable is a working MCP server with:
- MCP stdio transport as the default runtime (
npm start) reflect,feedback,library_info, andhealthtools- Optional MCP streamable HTTP mode (
npm run start:http) at/stillpoint/mcp - Content loaded from JSON files at startup
- In-memory session state with rate limiting
- Two-channel logging: structured JSON operational logs (stderr in MCP mode, stdout in HTTP mode), SQLite/Postgres database events
- Configuration via environment variables
- Input validation
- Safety tests against content library
That's the whole thing. Nothing else.
How to build it
Plain JavaScript. Not TypeScript. No build step. node server.js runs the server directly.
Express remains only for HTTP compatibility mode. Keep MCP as the primary interface.
better-sqlite3 for local database. pg for Postgres. dotenv for env loading in development. jest for tests. That's the full dependency list. Do not add anything else without an extremely specific reason.
No ORMs. Write SQL strings directly. There are two tables with five columns each. An ORM adds a dependency, an abstraction layer, and a learning curve for contributors, all to avoid writing ~20 lines of SQL.
What NOT to build
This section is more important than the one above.
Do not add TypeScript. No tsconfig.json, no dist/ directory, no type definitions, no compilation step. The project is a few hundred lines. Type safety is nice but not worth the tooling overhead here.
Do not add Docker. No Dockerfile, no docker-compose.yml. Local development is npm install && npm start. Heroku deployment is git push heroku main. Docker solves neither of these.
Do not add Redis. Session state is in-memory. A single Heroku dyno handles this workload. If Redis is ever needed, the session store interface is simple enough to swap later.
Do not create abstract interfaces, base classes, or factory patterns. There is one session store implementation (in-memory). There are two database backends (SQLite and Postgres) with a thin if/else in logger.js. Do not create AbstractStateStore, StateStoreFactory, DatabaseAdapter, or similar abstractions. Write the concrete implementation directly.
Do not create configuration profiles or presets. No profiles/public.json, profiles/research.json. Configuration is flat environment variables. Each feature is toggled by a single env var. The architecture doc lists them all.
Do not add middleware layers beyond what Express provides. No custom middleware framework. No request pipeline abstraction. Validation, rate limiting, and logging happen as function calls in the route handler, not as a chain of middleware objects.
Do not add CI/CD configuration. No .github/workflows/, no CircleCI, no Jenkinsfile. Tests run with npm test locally. CI can be added later in one file when the repo is ready for it.
Do not add linting or formatting configuration. No .eslintrc, no .prettierrc, no editorconfig. These are useful for teams but are not part of the application.
Do not add OpenAPI/Swagger. The MCP tool contract is documented in the architecture doc and README. A second machine-readable API spec adds maintenance cost with little value here.
Do not add a /readyz endpoint with downstream health checks. /health returns { status: "ok", library_version: "1.0.0" }. That's sufficient.
Do not add request IDs or correlation IDs. The operational log has a timestamp, route, status code, and situation. That's enough to debug a server that handles a few requests per minute.
Do not add graceful shutdown handlers. Session state is ephemeral. Database writes are fire-and-forget. There is nothing to drain or flush on shutdown.
Do not add data retention jobs, cleanup schedulers, or table partitioning. Session cleanup is a setInterval that evicts stale sessions from the in-memory store. Database rows accumulate. If retention ever matters, it's a future problem.
Do not add IP-based rate limiting. Rate limiting is per-session only, as described in the architecture doc.
Do not over-comment the code. The code is short and the variable names are descriptive. A comment that says // validate the situation parameter above a line that clearly validates the situation parameter adds nothing. Comment only when the why is not obvious from the what.
File structure
Follow this exactly. Do not add files beyond what's listed here unless the architecture doc specifically calls for them.
stillpoint/
├── server.js
├── config.js
├── lib/
│ ├── library.js
│ ├── sessions.js
│ ├── logger.js
│ └── validate.js
├── content/
│ ├── difficulty.json
│ ├── conflict.json
│ ├── uncertainty.json
│ ├── endings.json
│ ├── recognition.json
│ └── manifest.json
├── test/
│ ├── library.test.js
│ ├── api.test.js
│ ├── sessions.test.js
│ └── safety.test.js
├── .env.example
├── package.json
├── Procfile
└── README.md
No src/ directory. No dist/ directory. No scripts/ directory. No config/ directory.
Code style
Write plain, direct, readable JavaScript. Short functions. Flat structure. No class hierarchies.
Good:
function selectMessage(situation, session, library) {
const candidates = library.activeMessagesBySituation[situation];
if (!session || !session.history.length) {
return candidates[Math.floor(Math.random() * candidates.length)];
}
const recent = session.history.slice(-Math.floor(candidates.length / 2));
const available = candidates.filter(m => !recent.includes(m.id));
const pool = available.length > 0 ? available : candidates;
return pool[Math.floor(Math.random() * pool.length)];
}
Bad:
class ContentSelectionEngine {
constructor(libraryProvider, sessionManager, selectionStrategy) {
this.libraryProvider = libraryProvider;
this.sessionManager = sessionManager;
this.strategy = selectionStrategy || new DefaultRandomSelectionStrategy();
}
async select(context) {
const candidates = await this.libraryProvider.getActiveBySituation(context.situation);
const sessionState = await this.sessionManager.getHistory(context.sessionHash);
return this.strategy.execute(candidates, sessionState, context);
}
}
The first version does the same thing in 8 lines with no dependencies, no async, and no indirection. Write the first version.
Content files
The content JSON files should contain placeholder messages for now. Use the example content from stillpoint-v5.md (the five example messages per situation). The full library will be written separately. Each message needs id, status (set to "active"), content, and added_in fields.
The HMAC session hashing
When a session_name comes in, hash it with crypto.createHmac('sha256', secret).update(session_name).digest('hex') before using it as a key or logging it. The secret comes from LOG_HASH_SECRET env var. If not set, generate a random one at startup with crypto.randomBytes(32).toString('hex'). This is a few lines of code, not a module.
Testing
Tests should be runnable with npm test and nothing else. No test database setup, no Docker, no fixtures directory, no test utilities module. Jest runs against the code directly. The safety tests in safety.test.js are the most important tests in the project; they enforce the banned-pattern rules from the design doc against every message in the content library. Get those right.
Size check
When you're done, the total line count across server.js, config.js, and the four files in lib/ should be roughly 400-600 lines. If it's over 1000, something has gone wrong. Step back and simplify.
Summary of priorities
- It works:
npm startruns the MCP server andreflectreturns a message. - It's small: few hundred lines of application code, six dependencies.
- It's correct: safety tests pass, rate limiting works, sessions hash properly.
- It's readable: a new contributor can understand the entire codebase in 30 minutes.
Nothing else matters for v1.