#
tokens: 6244/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .env.template
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.env.template:
--------------------------------------------------------------------------------

```
ATTIO_API_KEY=ATTIO_API_KEY

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

.DS_Store
dist/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# attio-mcp-server

This is an MCP server for [Attio](https://attio.com/), the AI-native CRM. It allows mcp clients (like Claude) to connect to the Attio API.

#### Current Capabilities

- [x] reading company records
- [x] reading company notes
- [x] writing company notes
- [ ] other activities

## Usage

You will need:

- `ATTIO_API_KEY` 

This is expected to be a *bearer token* which means you can get one through the [API Explorer](https://developers.attio.com/reference/get_v2-objects) on the right hand side or configure OAuth and retrieve one throught the Attio API.


### Claude Desktop Configuration

```json
{
  "mcpServers": {
    "attio": {
      "command": "npx",
      "args": ["attio-mcp-server"],
      "env": {
        "ATTIO_API_KEY": "YOUR_ATTIO_API_KEY"
      }
    }
  }
}
```
## Development

### Prerequisites

Before you begin, ensure you have the following installed:

- Node.js (recommended v22 or higher)
- npm
- git
- dotenv

### Setting up Development Environment

To set up the development environment, follow these steps:

1. Fork the repository

   - Click the "Fork" button in the top-right corner of this repository
   - This creates your own copy of the repository under your Github acocunt

1. Clone Your Fork:

   ```sh
   git clone https://github.com/YOUR_USERNAME/attio-mcp-server.git
   cd attio-mcp-server
   ```

1. Add Upstream Remote
   ```sh
   git remote add upstream https://github.com/hmk/attio-mcp-server.git
   ```

1. Copy the dotenv file
    ```sh
    cp .env.template .env
    ```

1. Install dependencies:

   ```sh
   npm install
   ```

1. Run watch to keep index.js updated:

   ```sh
   npm run build:watch
   ```

1. Start the model context protocol development server:

   ```sh
   dotenv npx @modelcontextprotocol/inspector node PATH_TO_YOUR_CLONED_REPO/dist/index.js
   ```

1. If the development server did not load the environment variable correctly, set the `ATTIO_API_KEY` on the left-hand side of the mcp inspector.
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "attio-mcp-server",
  "version": "0.0.2",
  "description": "A Model Context Protocol server that connects Attio to LLMs",
  "main": "dist/index.js",
  "module": "dist/index.js",
  "type": "module",
  "access": "public",
  "bin": {
    "attio-mcp-server": "dist/index.js"
  },
  "scripts": {
    "clean": "shx rm -rf dist",
    "build": "tsc",
    "postbuild": "shx chmod +x dist/*.js",
    "check": "tsc --noEmit",
    "build:watch": "tsc --watch"
  },
  "files": [
    "dist"
  ],
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.4.1",
    "axios": "^1.7.9",
    "shx": "^0.3.4",
    "typescript": "^5.7.2"
  },
  "author": "@hmk",
  "license": "BSD-3-Clause",
  "devDependencies": {
    "@types/jest": "^29.5.14",
    "jest": "^29.7.0",
    "ts-jest": "^29.2.5",
    "tsx": "^4.19.2"
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "testPathIgnorePatterns": [
      "<rootDir>/dist/"
    ]
  }
}

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";

// Configure Axios instance with Attio API credentials from environment
const api = axios.create({
  baseURL: "https://api.attio.com/v2",
  headers: {
    "Authorization": `Bearer ${process.env.ATTIO_API_KEY}`,
    "Content-Type": "application/json",
  },
});

const server = new Server(
  {
    name: "attio-mcp-server",
    version: "0.0.1",
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  },
);

// Helper function to create detailed error responses
function createErrorResult(error: Error, url: string, method: string, responseData: any) {
  return {
    content: [
      {
        type: "text",
        text: `ERROR: ${error.message}\n\n` +
          `=== Request Details ===\n` +
          `- Method: ${method}\n` +
          `- URL: ${url}\n\n` +
          `=== Response Details ===\n` +
          `- Status: ${responseData.status}\n` +
          `- Headers: ${JSON.stringify(responseData.headers || {}, null, 2)}\n` +
          `- Data: ${JSON.stringify(responseData.data || {}, null, 2)}\n`
      },
    ],
    isError: true,
    error: {
      code: responseData.status || 500,
      message: error.message,
      details: responseData.data?.error || "Unknown error occurred"
    }
  };
}

// Example: List Resources Handler (List Companies)
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
  const path = "/objects/companies/records/query";
  try {
    const response = await api.post(path, {
      limit: 20,
      sorts: [{ attribute: 'last_interaction', field: 'interacted_at', direction: 'desc' }]
    });
    const companies = response.data.data || [];

    return {
      resources: companies.map((company: any) => ({
        uri: `attio://companies/${company.id?.record_id}`,
        name: company.values?.name?.[0]?.value || "Unknown Company",
        mimeType: "application/json",
      })),
      description: `Found ${companies.length} companies that you have interacted with most recently`,
    };
  } catch (error) {
    return createErrorResult(
      error instanceof Error ? error : new Error("Unknown error"),
      path,
      "POST",
      (error as any).response?.data || {}
    );
  }
});

// Example: Read Resource Handler (Get Company Details)
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const companyId = request.params.uri.replace("attio://companies/", "");
  try {
    const path = `/objects/companies/records/${companyId}`;
    const response = await api.get(path);

    return {
      contents: [
        {
          uri: request.params.uri,
          text: JSON.stringify(response.data, null, 2),
          mimeType: "application/json",
        },
      ],
    };
  } catch (error) {
    return createErrorResult(
      error instanceof Error ? error : new Error("Unknown error"),
      `/objects/companies/${companyId}`,
      "GET",
      (error as any).response?.data || {}
    );
  }
});

// Example: List Tools Handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "search-companies",
        description: "Search for companies by name",
        inputSchema: {
          type: "object",
          properties: {
            query: {
              type: "string",
              description: "Company name or keyword to search for",
            },
          },
          required: ["query"],
        },
      },
      {
        name: "read-company-details",
        description: "Read details of a company",
        inputSchema: {
          type: "object",
          properties: {
            uri: {
              type: "string",
              description: "URI of the company to read",
            },
          },
          required: ["uri"],
        },
      },
      {
        name: "read-company-notes",
        description: "Read notes for a company",
        inputSchema: {
          type: "object",
          properties: {
            uri: {
              type: "string",
              description: "URI of the company to read notes for",
            },
            limit: {
              type: "number",
              description: "Maximum number of notes to fetch (optional, default 10)",
            },
            offset: {
              type: "number",
              description: "Number of notes to skip (optional, default 0)",
            },
          },
          required: ["uri"],
        },
      },
      {
        name: "create-company-note",
        description: "Add a new note to a company",
        inputSchema: {
          type: "object",
          properties: {
            companyId: {
              type: "string",
              description: "ID of the company to add the note to",
            },
            noteTitle: {
              type: "string",
              description: "Title of the note",
            },
            noteText: {
              type: "string",
              description: "Text content of the note",
            },
          },
          required: ["companyId", "noteTitle", "noteText"],
        },
      },
    ],
  };
});

// Example: Call Tool Handler with enhanced error handling
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const toolName = request.params.name;
  try {

    if (toolName === "search-companies") {
      const query = request.params.arguments?.query as string;
      const path = "/objects/companies/records/query";
      try {
        const response = await api.post(path, {
          filter: {
            name: { "$contains": query },
          }
        });
        const results = response.data.data || [];

        const companies = results.map((company: any) => {
          const companyName = company.values?.name?.[0]?.value || "Unknown Company";
          const companyId = company.id?.record_id || "Record ID not found";
          return `${companyName}: attio://companies/${companyId}`;
        })
          .join("\n");
        return {
          content: [
            {
              type: "text",
              text: `Found ${results.length} companies:\n${companies}`,
            },
          ],
          isError: false,
        };
      } catch (error) {
        return createErrorResult(
          error instanceof Error ? error : new Error("Unknown error"),
          path,
          "GET",
          (error as any).response?.data || {}
        );
      }
    }

    if (toolName === "read-company-details") {
      const uri = request.params.arguments?.uri as string;
      const companyId = uri.replace("attio://companies/", "");
      const path = `/objects/companies/records/${companyId}`;
      try {
        const response = await api.get(path);
        return {
          content: [
            {
              type: "text",
              text: `Company details for ${companyId}:\n${JSON.stringify(response.data, null, 2)}`,
            },
          ],
          isError: false,
        };
      } catch (error) {
        return createErrorResult(
          error instanceof Error ? error : new Error("Unknown error"),
          path,
          "GET",
          (error as any).response?.data || {}
        );
      }
    }

    if (toolName == 'read-company-notes') {
      const uri = request.params.arguments?.uri as string;
      const limit = request.params.arguments?.limit as number || 10;
      const offset = request.params.arguments?.offset as number || 0;
      const companyId = uri.replace("attio://companies/", "");
      const path = `/notes?limit=${limit}&offset=${offset}&parent_object=companies&parent_record_id=${companyId}`;

      try {
        const response = await api.get(path);
        const notes = response.data.data || [];

        return {
          content: [
            {
              type: "text",
              text: `Found ${notes.length} notes for company ${companyId}:\n${notes.map((note: any) => JSON.stringify(note)).join("----------\n")}`,
            },
          ],
          isError: false,
        };
      } catch (error) {
        return createErrorResult(
          error instanceof Error ? error : new Error("Unknown error"),
          path,
          "GET",
          (error as any).response?.data || {}
        );
      }
    }

    if (toolName === "create-company-note") {
      const companyId = request.params.arguments?.companyId as string;
      const noteTitle = request.params.arguments?.noteTitle as string;
      const noteText = request.params.arguments?.noteText as string;
      const url = `notes`;

      try {
        const response = await api.post(url, {
          data: {
            format: "plaintext",
            parent_object: "companies",
            parent_record_id: companyId,
            title: `[AI] ${noteTitle}`,
            content: noteText
          },
        });

        return {
          content: [
            {
              type: "text",
              text: `Note added to company ${companyId}: attio://notes/${response.data?.id?.note_id}`,
            },
          ],
          isError: false,
        };
      } catch (error) {
        return createErrorResult(
          error instanceof Error ? error : new Error("Unknown error"),
          url,
          "POST",
          (error as any).response?.data || {}
        );
      }
    }

    throw new Error("Tool not found");
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Error executing tool '${toolName}': ${(error as Error).message}`,
        },
      ],
      isError: true,
    };
  }
});

// Main function
async function main() {
  try {
    if (!process.env.ATTIO_API_KEY) {
      throw new Error("ATTIO_API_KEY environment variable not found");
    }

    const transport = new StdioServerTransport();
    await server.connect(transport);
  } catch (error) {
    console.error("Error starting server:", error);
    process.exit(1);
  }
}

main().catch(error => {
  console.error("Unhandled error:", error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
    "compilerOptions": {
      /* Visit https://aka.ms/tsconfig to read more about this file */
  
      /* Projects */
      // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
      // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
      // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
      // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
      // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
      // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
  
      /* Language and Environment */
      "target": "ES2022",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
      // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
      // "jsx": "preserve",                                /* Specify what JSX code is generated. */
      // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
      // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
      // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
      // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
      // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
      // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
      // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
      // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
      // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
  
      /* Modules */
      "module": "NodeNext",                                /* Specify what module code is generated. */
      "rootDir": "./src",                                  /* Specify the root folder within your source files. */
      "moduleResolution": "NodeNext",                     /* Specify how TypeScript looks up a file from a given module specifier. */
      // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
      "paths": {
        "*": ["./node_modules/@types/*"]
      },                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
      // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
      // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
      // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
      // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
      // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
      // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
      // "rewriteRelativeImportExtensions": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
      // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
      // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
      // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
      // "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
      "resolveJsonModule": true,                        /* Enable importing .json files. */
      // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
      // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
  
      /* JavaScript Support */
      "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
      // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
      // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
  
      /* Emit */
      "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
      "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
      // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
      "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
      // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
      // "noEmit": true,                                   /* Disable emitting files from a compilation. */
      // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
      "outDir": "./dist",                                   /* Specify an output folder for all emitted files. */
      // "removeComments": true,                           /* Disable emitting comments. */
      // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
      // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
      // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
      // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
      // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
      // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
      // "newLine": "crlf",                                /* Set the newline character for emitting files. */
      // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
      // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
      // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
      // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
      // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
  
      /* Interop Constraints */
      "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
      // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
      // "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
      // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
      "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
      // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
      "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
  
      /* Type Checking */
      "strict": true,                                      /* Enable all strict type-checking options. */
      // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
      // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
      // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
      // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
      // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
      // "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
      // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
      // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
      // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
      // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
      // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
      // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
      // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
      // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
      // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
      // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
      // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
      // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
      // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
  
      /* Completeness */
      // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
      "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "__tests__"]
  }
  
```