Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ workflows, and MCP access from one cohesive application.
on mobile and desktop.
- Receive optional weekly and monthly email summaries with OpenAI-generated
spending and income insights.
- Connect external tools through API keys, a typed Node client, a read-only MCP
- Connect external tools through API keys, a typed Node client, an MCP
server, and a Telegram bot.

## Built With Cleverbrush
Expand Down Expand Up @@ -250,9 +250,11 @@ subtract it from that category.

## MCP Server

xpenser exposes a read-only MCP Streamable HTTP endpoint for AI agents at
xpenser exposes an MCP Streamable HTTP endpoint for AI agents at
`/external-api/mcp`. Use the same API key from Settings -> Preferences -> API
keys as a bearer token:
keys as a bearer token. MCP tools can read and manage the API-key owner's
vendors, categories, and transactions, so treat MCP access as full account data
access:

```json
{
Expand All @@ -268,9 +270,9 @@ keys as a bearer token:
}
```

The MCP server exposes read-only tools for the current user, categories,
transactions, dashboard summaries, and statistics. Transaction write operations
are not exposed through MCP.
The MCP server exposes tools for the current user, vendors, categories,
transactions, dashboard summaries, and statistics. Vendor candidate search and
enrichment may call the configured vendor enrichment provider.

## Development Commands

Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ fixes before public disclosure.
- Passwords use scrypt with per-password salts.
- API keys, Telegram link tokens, and email confirmation tokens are stored as
hashes.
- MCP access requires an API-key principal and exposes read-only tools.
- MCP access requires an API-key principal and can read or mutate the API-key
owner's vendors, categories, and transactions.
- Database spans redact SQL text at the instrumentation boundary.
15 changes: 11 additions & 4 deletions apps/api/src/mcp/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { ActionResult, endpoint, type Handler } from '@cleverbrush/server';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { PrincipalSchema } from '@xpenser/contracts';
import { DbToken, LoggerToken } from '../di/tokens.js';
import { ConfigToken, DbToken, KnexToken, LoggerToken } from '../di/tokens.js';
import { McpTransportError } from '../log-templates.js';
import { requireMcpApiKeyPrincipal } from './auth.js';
import { createXpenserMcpServer } from './server.js';

export const McpEndpoint = endpoint
.post('/api/mcp')
.authorize(PrincipalSchema)
.inject({ db: DbToken, logger: LoggerToken })
.inject({
config: ConfigToken,
db: DbToken,
knex: KnexToken,
logger: LoggerToken
})
.summary('MCP server')
.description('Read-only xpenser MCP server for AI agents.')
.description('xpenser MCP server for AI agents.')
.tags('mcp')
.operationId('xpenserMcp');

export const mcpHandler: Handler<typeof McpEndpoint> = async (
{ context, principal },
{ db, logger }
{ config, db, knex, logger }
) => {
let apiKeyPrincipal: ReturnType<typeof requireMcpApiKeyPrincipal>;
try {
Expand All @@ -30,7 +35,9 @@ export const mcpHandler: Handler<typeof McpEndpoint> = async (
}

const mcpServer = createXpenserMcpServer({
config,
db,
knex,
logger,
principal: apiKeyPrincipal
});
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/mcp/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Logger } from '@cleverbrush/log';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Knex } from 'knex';
import type { Config } from '../config.js';
import type { AppDb } from '../db/schemas.js';
import type { McpApiKeyPrincipal } from './auth.js';
import {
Expand All @@ -8,13 +10,17 @@ import {
} from './tools.js';

export type XpenserMcpServerOptions = {
readonly config: Config;
readonly db: AppDb;
readonly knex: Knex;
readonly logger: Logger;
readonly principal: McpApiKeyPrincipal;
};

export function createXpenserMcpServer({
config,
db,
knex,
logger,
principal
}: XpenserMcpServerOptions): Server {
Expand All @@ -26,7 +32,7 @@ export function createXpenserMcpServer({
registerXpenserMcpTools(server, {
principal,
logger,
data: createXpenserMcpDataAccess(db)
data: createXpenserMcpDataAccess(db, config, knex)
});

return server;
Expand Down
Loading
Loading