users
# typescript-mcp-server
MCP servers for the Portfolio API, served over HTTP by one Express app.
| Endpoint | Server | Tools |
| -------------------- | ------------------ | --------------------------------------- |
| `/users/mcp` | `users-mcp` | `get_user_by_email`, `get_user_by_name` |
| `/accounts/mcp` | `accounts-mcp` | `get_accounts_by_user_id` |
| `/transactions/mcp` | `transactions-mcp` | `get_transactions_by_account_id` |
The tools chain: an email resolves to a user id, which resolves to accounts,
whose ids resolve to transactions.
Default port: `4000`. VS Code is wired to both in [.vscode/mcp.json](.vscode/mcp.json).
## Scripts
| Command | Purpose |
| ------------------- | ----------------------------------- |
| `npm run dev` | Run from source with `tsx` |
| `npm run build` | Compile to `dist/` |
| `npm start` | Run the compiled build |
| `npm run typecheck` | Type-check without emitting |
## Configuration
Copy `.env.example` to `.env`. All variables are declared and validated in
[src/config.ts](src/config.ts); startup fails with a readable message if any
are invalid.
| Variable | Default | Purpose |
| -------------------- | ----------------------- | -------------------------------------- |
| `NODE_ENV` | `development` | `development` enables pretty logs |
| `LOG_LEVEL` | `info` | `debug`/`info`/`warn`/`error`/`silent` |
| `PORTFOLIO_API_BASE` | `http://localhost:3000` | Base URL of the Portfolio API |
## Layout
```
src/
index.ts Mounts every MCP server on the Express app
config.ts Env vars, loaded and validated once
logger.ts pino logger (pretty in development)
api.ts Shared axios client for the Portfolio API
tool-result.ts Builds the CallToolResult shape
servers/
users/
index.ts createUsersServer(): registers the tools
users.api.ts API calls that return data
users.schema.ts zod schemas for input and output
accounts/
index.ts createAccountsServer()
accounts.api.ts
accounts.schema.ts
transactions/
index.ts createTransactionsServer()
transactions.api.ts
transactions.schema.ts
```
### Logging
Use the `logger` from [src/logger.ts](src/logger.ts), which writes to stderr.
Avoid `console.log`: it writes to stdout, which is the JSON-RPC wire if this
project is ever switched back to the stdio transport.
### Errors
Tool handlers do not need `try`/`catch`. The MCP SDK catches anything thrown
and returns it as a tool result flagged `isError`, so an API call can simply
throw when there is no match.
### Schemas
`outputSchema` is enforced: the SDK validates `structuredContent` against it
and fails the call on a mismatch — but it discards the parsed value, so any
normalising has to happen in the `.api.ts` file before the data is returned.
json-server returns each primary key as a **string** (`"id": "1"`) while
leaving foreign keys **numeric** (`"userId": 1`). The schemas use
`z.coerce.number()` on ids to smooth that over; they still advertise a plain
integer. Check a new endpoint with `curl` rather than trusting `db.json` —
json-server rewrites the ids it loads from there.
## Adding a tool
Add the API call to `<server>.api.ts`, its schemas to `<server>.schema.ts`, and
a `server.registerTool(...)` block in that server's `index.ts`.
## Adding another MCP server
Create `src/servers/<name>/` with the same three files, exporting a
`create<Name>Server(): McpServer`, then add one line to the `MCP_SERVERS` array
in [src/index.ts](src/index.ts).
TDQS
Scored across 2 tools
The two tools are clearly distinguished by their lookup parameter: email vs. name. Descriptions explicitly state each input, so an agent can confidently select the appropriate tool without ambiguity.
Both tools follow the same verb_noun_by_attribute pattern (get_user_by_...). This is consistent and predictable, making it easy to infer behavior from the name.
With only 2 tools, the server feels thin, but the scope may be intentionally limited to user lookup. It is not as extreme as having a single trivial tool, so it sits at the borderline.
The server only provides read operations for users. There are no create, update, delete, or list endpoints, which are essential for a 'users' domain. This is a significant functional gap that will hinder agents needing full user lifecycle management.