Give your finance team a connector and they can ask their assistant for an invoice's status, an expense's state, or whether an approval cleared, over your own finance systems, without exposing those systems to the open internet. The connector is a hosted MCP server behind hosted OAuth. What makes it a finance connector is restraint: the tools read far more than they write, the credentials are write-only, every access is logged, and the backend cannot phone home. This post is about that team use case and the trust story around it, not a deep dive on fetching a single invoice.
Why should a finance connector be read-mostly?
Because the value is in answering questions, and the risk is in changing records. "Is invoice 4021 paid?" and "did the Q3 travel expense get approved?" are lookups. They save real time and carry almost no downside. Mutating a ledger from a chat window is a different category of action, and most of it should stay in the finance system's own controlled workflows.
So the default posture is read. The connector below exposes three read tools. If you later add a write, you add exactly one narrow tool for it, deliberately, not a broad "update" surface the assistant can point anywhere.
import { defineMcp } from "@denkopsai/sdk/mcp";
import { z } from "zod";
const FIN = process.env.FINANCE_API_URL!;
async function fin(path: string) {
const res = await fetch(`${FIN}${path}`, {
headers: { authorization: `Bearer ${process.env.FINANCE_TOKEN}` },
});
if (!res.ok) throw new Error(`finance system answered ${res.status}`);
return res.json();
}
export default defineMcp({
name: "finance-tools",
version: "1.0.0",
tools: {
lookup_invoice: {
description: "Get an invoice and its payment status by invoice number.",
input: z.object({ invoiceNumber: z.string() }),
handler: ({ invoiceNumber }) => fin(`/invoices/${invoiceNumber}`),
},
expense_status: {
description: "Get the status of an expense report by its id.",
input: z.object({ expenseId: z.string() }),
handler: ({ expenseId }) => fin(`/expenses/${expenseId}`),
},
approval_check: {
description: "Check whether a request has cleared its approval chain.",
input: z.object({ requestId: z.string() }),
handler: ({ requestId }) => fin(`/approvals/${requestId}`),
},
},
});Each tool has a typed input, so the assistant knows what to pass and a wrong call fails clearly before your handler runs.
How do write-only secrets protect a finance backend?
The FINANCE_TOKEN your tools use is read from process.env and, on DenkOps, is write-only after you save it. You paste it once, it is stored encrypted, and after that nothing reads it back: not the dashboard, not the CLI, not an assistant, not an agent that helped write the code. The only place the plaintext exists is inside the running process. For a finance credential that is the difference between a secret and a secret that keeps ending up in transcripts and logs.
Why does a finance backend need zero-trust egress?
Because a finance backend should not phone home. Its whole job is to talk to your finance system and answer the caller. It has no legitimate reason to open a connection to some other host, and if it ever tries, that is a signal, not a feature.
DenkOps enforces exactly this. Outbound calls are limited to a host allowlist you control. You put your finance system on the list and nothing else. If a compromised dependency or a prompt-injected instruction gets the backend to request an attacker's URL, that host is not allowed, so the request fails and shows up in your logs instead of leaking figures. The connector can reach your books and only your books.
How do I prove who saw what?
With access control plus a log. Set "connector": true for hosted OAuth, then restrict the project to your finance team with an allowed-email list. Every grant and membership change is recorded in the workspace audit log, so "who could query invoices last quarter" is answerable, which is the kind of question finance actually gets asked.
{ "name": "finance-tools", "slug": "finance-tools", "runtime": "bun", "connector": true }Read-mostly tools, sealed credentials, a network that cannot phone home, and a log of every grant: that is a finance connector your team can use and your auditor can accept. See the security overview for the platform defaults it builds on, then start from deploying your first MCP server. The same restraint pattern fits a support connector too.