Blog · 2026-08-10

A CRM connector your sales team can talk to

Let your sales team look up an account, log an activity, and get next steps from their assistant, over your own CRM, with hosted OAuth and per-team access.

Give your sales team a connector and they can talk to your CRM in plain language: "pull up the Acme account," "log that I called them today," "what are the next steps here?" The connector is a hosted MCP server the assistant installs from one URL. You write three tools that read and write your existing CRM; DenkOps hosts them, runs the login, and limits access to the sales team. Be clear about the split up front: you write the CRM logic, DenkOps hosts and secures it.

Why route the CRM through a connector instead of the CRM's own app?

Because your reps do not live in the CRM, they live in email, calls, and their assistant, and the CRM is the tab they forget to update. A connector puts three high-frequency actions where the work already happens. A rep finishing a call says "log a call with Acme, they want a security review before signing," and the activity is written without a context switch. The friction that makes CRM data go stale is exactly the friction a connector removes.

The tools are yours, pointed at your CRM. Whether that CRM is a hosted product with an API or a database your team runs, the connector is a thin layer that reads and writes it on the rep's behalf.

What tools does a sales connector need?

Three cover most of the day: look up an account, log an activity, and get next steps. The connector reads its CRM credentials from process.env and never exposes them to the assistant.

import { defineMcp } from "@denkopsai/sdk/mcp";
import { z } from "zod";

const CRM = process.env.CRM_BASE_URL!;

async function crm(path: string, init?: RequestInit) {
  const res = await fetch(`${CRM}${path}`, {
    ...init,
    headers: {
      authorization: `Bearer ${process.env.CRM_API_KEY}`,
      "content-type": "application/json",
    },
  });
  if (!res.ok) throw new Error(`CRM answered ${res.status}`);
  return res.json();
}

export default defineMcp({
  name: "crm-tools",
  version: "1.0.0",
  tools: {
    lookup_account: {
      description: "Look up an account by company name.",
      input: z.object({ name: z.string() }),
      handler: ({ name }) => crm(`/accounts?q=${encodeURIComponent(name)}`),
    },
    log_activity: {
      description: "Log an activity (call, email, meeting) against an account.",
      input: z.object({
        accountId: z.string(),
        type: z.enum(["call", "email", "meeting"]),
        note: z.string(),
      }),
      handler: (a) =>
        crm("/activities", { method: "POST", body: JSON.stringify(a) }),
    },
    next_steps: {
      description: "Get open tasks and suggested next steps for an account.",
      input: z.object({ accountId: z.string() }),
      handler: ({ accountId }) => crm(`/accounts/${accountId}/next-steps`),
    },
  },
});

The type enum on log_activity is not decoration. The assistant reads that Zod schema before it calls, so it knows the three legal activity types and fills them correctly, and a malformed call is rejected with a readable error before your handler runs.

Who wrote what here?

Split it honestly. You bring the CRM and write the tool bodies: which endpoints to call, what an account looks like, what "next steps" means for your pipeline. That is real work and only you can do it, because it is your data model.

DenkOps supplies the parts around it. "connector": true in denkops.json gives you the entire OAuth flow, discovery through consent, with zero auth code in your app. Your CRM credentials live in write-only env vars that authenticate the tools but never leak to the assistant. And your CRM's host goes on a zero-trust egress allowlist, so the connector can reach your CRM and nothing else, no matter what a prompt tries.

{ "name": "crm-tools", "slug": "crm-tools", "runtime": "bun", "connector": true }

How do I keep it to the sales team?

Set the project's access policy to an allowed-email list and add your reps. Only they get past consent; everyone else is refused at login. Grants and membership changes are recorded in the workspace audit log, so a connector that can write to your CRM stays accountable, the same way it does for a support connector. When a rep leaves, remove them from the list, no redeploy needed. The same URL installs the same way in Claude, ChatGPT, and Claude Code.

Give your reps a CRM they can talk to. Write the three tools, deploy, and share the URL with the team. Start from deploying your first MCP server.

FAQ

Do I have to move my CRM to use this?

No. You keep your CRM where it is. The connector calls it through your existing API or database, using credentials in an environment variable, so your data model and system of record do not move. What changes is that the assistant can reach a few chosen actions.

What does DenkOps handle versus what I build?

You build the tool logic against your CRM: the queries, writes, and shapes. DenkOps hosts the server on a stable HTTPS URL, runs the full OAuth flow behind `"connector": true`, keeps your CRM credentials write-only, and restricts outbound calls to the hosts you allow.

Can a rep who leaves still reach the CRM connector?

No, once you remove them from the allowed-email list they are refused at login. Access is a list you control in the dashboard, separate from the code, and every change is captured in the workspace audit log.

Ship it yourself: bunx denkops deploy or say "deploy on DenkOps" from your coding agent.

Start on DenkOps →

← Build a Python agent backend on DenkOps · TTL cache patterns with denkops.store →