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

```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── company
│   │   ├── get_companies.ts
│   │   └── get_company_detail.ts
│   ├── component
│   │   ├── get_component_detail.ts
│   │   └── get_components.ts
│   ├── feature
│   │   ├── get_feature_detail.ts
│   │   └── get_features.ts
│   ├── feature_status
│   │   └── get_feature_statuses.ts
│   ├── index.ts
│   ├── note
│   │   ├── get_note_detail.ts
│   │   └── get_notes.ts
│   ├── product
│   │   ├── get_product_detail.ts
│   │   └── get_products.ts
│   └── productboard_client.ts
└── tsconfig.json
```

# Files

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

```
node_modules/
build/
*.log
.env*
```

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

```markdown
# Productboard MCP Server

Integrate the Productboard API into agentic workflows via MCP


## Tools

1. `get_companies`
2. `get_company_detail`
3. `get_components`
4. `get_component_detail`
5. `get_features`
6. `get_feature_detail`
7. `get_feature_statuses`
8. `get_notes`
9. `get_products`
10. `get_product_detail`


## Setup

### Access Token
Obtain your access token referring to [this guidance](https://developer.productboard.com/reference/authentication#public-api-access-token)

### Usage with Claude Desktop
To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:

### NPX

```json
{
  "mcpServers": {
    "productboard": {
      "command": "npx",
      "args": [
        "-y",
        "productboard-mcp"
      ],
      "env": {
        "PRODUCTBOARD_ACCESS_TOKEN": "<YOUR_TOKEN>"
      }
    }
  }
}
```

## License

This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/src/note/get_note_detail.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getNoteDetailTool: Tool = {
    "name": "get_note_detail",
    "description": "Returns detailed information about a specific note",
    "inputSchema": {
        "type": "object",
        "properties": {
            "noteId": {
                "type": "string",
                "description": "ID of the note to retrieve"
            }
        },
        "required": ["noteId"]
    }
}

interface GetNoteDetailRequest {
    noteId: string
}

const getNoteDetail = async (request: GetNoteDetailRequest): Promise<any> => {
    const endpoint = `/notes/${request.noteId}`
    return productboardClient.get(endpoint)
}

export { getNoteDetailTool, GetNoteDetailRequest, getNoteDetail }

```

--------------------------------------------------------------------------------
/src/feature/get_feature_detail.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getFeatureDetailTool: Tool = {
    "name": "get_feature_detail",
    "description": "Returns detailed information about a specific feature",
    "inputSchema": {
        "type": "object",
        "properties": {
            "featureId": {
                "type": "string",
                "description": "ID of the feature to retrieve"
            }
        },
        "required": ["featureId"]
    }
}

interface GetFeatureDetailRequest {
    featureId: string
}

const getFeatureDetail = async (request: GetFeatureDetailRequest): Promise<any> => {
    const endpoint = `/features/${request.featureId}`
    return productboardClient.get(endpoint)
}

export { getFeatureDetailTool, GetFeatureDetailRequest, getFeatureDetail }

```

--------------------------------------------------------------------------------
/src/product/get_product_detail.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getProductDetailTool: Tool = {
    "name": "get_product_detail",
    "description": "Returns detailed information about a specific product",
    "inputSchema": {
        "type": "object",
        "properties": {
            "productId": {
                "type": "string",
                "description": "ID of the product to retrieve"
            }
        },
        "required": ["productId"]
    }
}

interface GetProductDetailRequest {
    productId: string
}

const getProductDetail = async (request: GetProductDetailRequest): Promise<any> => {
    const endpoint = `/products/${request.productId}`
    return productboardClient.get(endpoint)
}

export { getProductDetailTool, GetProductDetailRequest, getProductDetail }

```

--------------------------------------------------------------------------------
/src/company/get_company_detail.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getCompanyDetailTool: Tool = {
    "name": "get_company_detail",
    "description": "Returns detailed information about a specific company",
    "inputSchema": {
        "type": "object",
        "properties": {
            "companyId": {
                "type": "string",
                "description": "ID of the company to retrieve"
            }
        },
        "required": ["companyId"]
    }
}

interface GetCompanyDetailRequest {
    companyId: string
}

const getCompanyDetail = async (request: GetCompanyDetailRequest): Promise<any> => {
    const endpoint = `/companies/${request.companyId}`
    return productboardClient.get(endpoint)
}

export { getCompanyDetailTool, GetCompanyDetailRequest, getCompanyDetail }

```

--------------------------------------------------------------------------------
/src/feature/get_features.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getFeaturesTool: Tool = {
    "name": "get_features",
    "description": "Returns a list of all features. This API is paginated and the page limit is always 100",
    "inputSchema": {
        "type": "object",
        "properties": {
            "page": {
                "type": "number",
                "default": 1
            }
        }
    }
}

interface GetFeaturesRequest {
    page?: number
}

const getFeatures = async (request: GetFeaturesRequest): Promise<any> => {
    let endpoint = "/features"
    if (request.page && request.page > 1) {
        endpoint += `?pageOffset=${(request.page - 1) * 100}`
    }

    return productboardClient.get(endpoint)
}

export { getFeaturesTool, GetFeaturesRequest, getFeatures }

```

--------------------------------------------------------------------------------
/src/product/get_products.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getProductsTool: Tool = {
    "name": "get_products",
    "description": "Returns detail of all products. This API is paginated and the page limit is always 100",
    "inputSchema": {
        "type": "object",
        "properties": {
            "page": {
                "type": "number",
                "default": 1
            }
        }
    }
}

interface GetProductsRequest {
    page?: number
}

const getProducts = async (request: GetProductsRequest): Promise<any> => {
    let endpoint = "/products"
    if (request.page && request.page > 1) {
        endpoint += `?pageOffset=${(request.page - 1) * 100}`
    }

    return productboardClient.get(endpoint)
}

export { getProductsTool, GetProductsRequest, getProducts }

```

--------------------------------------------------------------------------------
/src/company/get_companies.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getCompaniesTool: Tool = {
    "name": "get_companies",
    "description": "Returns a list of all companies. This API is paginated and the page limit is always 100",
    "inputSchema": {
        "type": "object",
        "properties": {
            "page": {
                "type": "number",
                "default": 1
            }
        }
    }
}

interface GetCompaniesRequest {
    page?: number
}

const getCompanies = async (request: GetCompaniesRequest): Promise<any> => {
    let endpoint = "/companies"
    if (request.page && request.page > 1) {
        endpoint += `?pageOffset=${(request.page - 1) * 100}`
    }

    return productboardClient.get(endpoint)
}

export { getCompaniesTool, GetCompaniesRequest, getCompanies }

```

--------------------------------------------------------------------------------
/src/component/get_components.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getComponentsTool: Tool = {
    "name": "get_components",
    "description": "Returns a list of all components. This API is paginated and the page limit is always 100",
    "inputSchema": {
        "type": "object",
        "properties": {
            "page": {
                "type": "number",
                "default": 1
            }
        }
    }
}

interface GetComponentsRequest {
    page?: number
}

const getComponents = async (request: GetComponentsRequest): Promise<any> => {
    let endpoint = "/components"
    if (request.page && request.page > 1) {
        endpoint += `?pageOffset=${(request.page - 1) * 100}`
    }

    return productboardClient.get(endpoint)
}

export { getComponentsTool, GetComponentsRequest, getComponents }

```

--------------------------------------------------------------------------------
/src/component/get_component_detail.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getComponentDetailTool: Tool = {
    "name": "get_component_detail",
    "description": "Returns detailed information about a specific component",
    "inputSchema": {
        "type": "object",
        "properties": {
            "componentId": {
                "type": "string",
                "description": "ID of the component to retrieve"
            }
        },
        "required": ["componentId"]
    }
}

interface GetComponentDetailRequest {
    componentId: string
}

const getComponentDetail = async (request: GetComponentDetailRequest): Promise<any> => {
    const endpoint = `/components/${request.componentId}`
    return productboardClient.get(endpoint)
}

export { getComponentDetailTool, GetComponentDetailRequest, getComponentDetail }

```

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

```json
{
  "name": "productboard-mcp",
  "version": "1.0.1",
  "description": "Integrate the Productboard API into agentic workflows via MCP",
  "author": "Kenji Hikmatullah",
  "license": "MIT",
  "publisher": "kenjihikmatullah",
  "repository": {
    "type": "git",
    "url": "https://github.com/kenjihikmatullah/productboard-mcp"
  },
  "type": "module",
  "bin": {
    "notion": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "inspector": "npx @modelcontextprotocol/inspector build/index.js"
  },
  "keywords": [
    "mcp",
    "agent",
    "autonomous",
    "ai"
  ],
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.6.0"
  },
  "devDependencies": {
    "@types/node": "^22.13.5",
    "typescript": "^5.7.3"
  }
}

```

--------------------------------------------------------------------------------
/src/feature_status/get_feature_statuses.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getFeatureStatusesTool: Tool = {
    "name": "get_feature_statuses",
    "description": "Returns a list of all feature statuses. This API is paginated and the page limit is always 100",
    "inputSchema": {
        "type": "object",
        "properties": {
            "page": {
                "type": "number",
                "default": 1
            }
        }
    }
}

interface GetFeatureStatusesRequest {
    page?: number
}

const getFeatureStatuses = async (request: GetFeatureStatusesRequest): Promise<any> => {
    let endpoint = "/feature-statuses"
    if (request.page && request.page > 1) {
        endpoint += `?pageOffset=${(request.page - 1) * 100}`
    }

    return productboardClient.get(endpoint)
}

export { getFeatureStatusesTool, GetFeatureStatusesRequest, getFeatureStatuses }

```

--------------------------------------------------------------------------------
/src/productboard_client.ts:
--------------------------------------------------------------------------------

```typescript
class ProductboardClient {
    private accessToken: string
    private baseUrl = "https://api.productboard.com"
    private headers: { [key: string]: string };

    constructor(accessToken: string) {
        this.accessToken = accessToken
        this.headers = {
            Authorization: `Bearer ${this.accessToken}`,
            "Content-Type": "application/json",
            "Accept": "application/json",
            "X-Version": "1",
        };
    }

    async get(endpoint: string) {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            method: "GET",
            headers: this.headers,
        });
        return response.json()
    }
}

const productboardAccessToken = process.env.PRODUCTBOARD_ACCESS_TOKEN

if (!productboardAccessToken) {
    console.error("Please set PRODUCTBOARD_ACCESS_TOKEN environment variable");
    process.exit(1);
}

const productboardClient = new ProductboardClient(process.env.PRODUCTBOARD_ACCESS_TOKEN!)
export default productboardClient

```

--------------------------------------------------------------------------------
/src/note/get_notes.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import productboardClient from "../productboard_client.js";

const getNotesTool: Tool = {
    "name": "get_notes",
    "description": "Returns a list of all notes",
    "inputSchema": {
        "type": "object",
        "properties": {
            "last": {
                "type": "string",
                "description": "Return only notes created since given span of months (m), days (s), or hours (h). E.g. 6m | 10d | 24h | 1h. Cannot be combined with createdFrom, createdTo, dateFrom, or dateTo"
            },
            "createdFrom": {
                "type": "string",
                "format": "date",
                "description": "Return only notes created since given date. Cannot be combined with last"
            },
            "createdTo": {
                "type": "string",
                "format": "date",
                "description": "Return only notes created before or equal to the given date. Cannot be combined with last"
            },
            "updatedFrom": {
                "type": "string",
                "format": "date",
                "description": "Return only notes updated since given date"
            },
            "updatedTo": {
                "type": "string",
                "format": "date",
                "description": "Return only notes updated before or equal to the given date"
            },
            "term": {
                "type": "string",
                "description": "Return only notes by fulltext search"
            },
            "featureId": {
                "type": "string",
                "description": "Return only notes for specific feature ID or its descendants"
            },
            "companyId": {
                "type": "string",
                "description": "Return only notes for specific company ID"
            },
            "ownerEmail": {
                "type": "string",
                "description": "Return only notes owned by a specific owner email"
            },
            "source": {
                "type": "string",
                "description": "Return only notes from a specific source origin. This is the unique string identifying the external system from which the data came"
            },
            "anyTag": {
                "type": "string",
                "description": "Return only notes that have been assigned any of the tags in the array. Cannot be combined with allTags"
            },
            "allTags": {
                "type": "string",
                "description": "Return only notes that have been assigned all of the tags in the array. Cannot be combined with anyTag"
            },
            "pageLimit": {
                "type": "number",
                "description": "Page limit"
            },
            "pageCursor": {
                "type": "string",
                "description": "Page cursor to get next page of results"
            }
        }
    }
}

interface GetNotesRequest {
    last?: string;
    createdFrom?: string;
    createdTo?: string;
    updatedFrom?: string;
    updatedTo?: string;
    term?: string;
    featureId?: string;
    companyId?: string;
    ownerEmail?: string;
    source?: string;
    anyTag?: string;
    allTags?: string;
    pageLimit?: number;
    pageCursor?: string;
}

const getNotes = async (request: GetNotesRequest): Promise<any> => {
    // Validate mutually exclusive parameters
    if (request.last && (request.createdFrom || request.createdTo)) {
        throw new Error("'last' parameter cannot be combined with 'createdFrom' or 'createdTo'");
    }
    if (request.anyTag && request.allTags) {
        throw new Error("'anyTag' cannot be combined with 'allTags'");
    }

    const params = new URLSearchParams()
    
    if (request.last) {
        params.append('last', request.last)
    }
    if (request.createdFrom) {
        params.append('createdFrom', request.createdFrom)
    }
    if (request.createdTo) {
        params.append('createdTo', request.createdTo)
    }
    if (request.updatedFrom) {
        params.append('updatedFrom', request.updatedFrom)
    }
    if (request.updatedTo) {
        params.append('updatedTo', request.updatedTo)
    }
    if (request.term) {
        params.append('term', request.term)
    }
    if (request.featureId) {
        params.append('featureId', request.featureId)
    }
    if (request.companyId) {
        params.append('companyId', request.companyId)
    }
    if (request.ownerEmail) {
        params.append('ownerEmail', request.ownerEmail)
    }
    if (request.source) {
        params.append('source', request.source)
    }
    if (request.anyTag) {
        params.append('anyTag', request.anyTag)
    }
    if (request.allTags) {
        params.append('allTags', request.allTags)
    }
    if (request.pageLimit) {
        params.append('pageLimit', request.pageLimit.toString())
    }
    if (request.pageCursor) {
        params.append('pageCursor', request.pageCursor)
    }

    const queryString = params.toString()
    const endpoint = `/notes${queryString ? `?${queryString}` : ''}`

    return productboardClient.get(endpoint)
}

export { getNotesTool, GetNotesRequest, getNotes }

```

--------------------------------------------------------------------------------
/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 {
    CallToolRequest,
    CallToolRequestSchema,
    ListToolsRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { getProductsTool, GetProductsRequest, getProducts } from "./product/get_products.js";
import { getProductDetailTool, GetProductDetailRequest, getProductDetail } from "./product/get_product_detail.js";
import { getFeaturesTool, GetFeaturesRequest, getFeatures } from "./feature/get_features.js";
import { getFeatureDetailTool, GetFeatureDetailRequest, getFeatureDetail } from "./feature/get_feature_detail.js";
import { getComponentsTool, GetComponentsRequest, getComponents } from "./component/get_components.js";
import { getComponentDetailTool, GetComponentDetailRequest, getComponentDetail } from "./component/get_component_detail.js";
import { getFeatureStatusesTool, GetFeatureStatusesRequest, getFeatureStatuses } from "./feature_status/get_feature_statuses.js";
import { getNotesTool, GetNotesRequest, getNotes } from "./note/get_notes.js";
import { getNoteDetailTool, GetNoteDetailRequest, getNoteDetail } from "./note/get_note_detail.js";
import { getCompaniesTool, GetCompaniesRequest, getCompanies } from "./company/get_companies.js";
import { getCompanyDetailTool, GetCompanyDetailRequest, getCompanyDetail } from "./company/get_company_detail.js";

async function main() {
    const productboardAccessToken = process.env.PRODUCTBOARD_ACCESS_TOKEN

    if (!productboardAccessToken) {
        console.error("Please set PRODUCTBOARD_ACCESS_TOKEN environment variable");
        process.exit(1);
    }

    const server = new Server(
        {
            name: "Productboard MCP Server",
            version: "1.0.0",
        },
        {
            capabilities: {
                tools: {},
            },
        }
    );

    server.setRequestHandler(
        CallToolRequestSchema,
        async (request: CallToolRequest) => {
            console.info("Received CallToolRequest: ", request);

            try {
                const { name, arguments: args } = request.params

                switch (name) {
                    case getProductsTool.name: {
                        const request = args as unknown as GetProductsRequest;
                        const result = await getProducts(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getProductDetailTool.name: {
                        const request = args as unknown as GetProductDetailRequest;
                        const result = await getProductDetail(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getFeaturesTool.name: {
                        const request = args as unknown as GetFeaturesRequest;
                        const result = await getFeatures(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getFeatureDetailTool.name: {
                        const request = args as unknown as GetFeatureDetailRequest;
                        const result = await getFeatureDetail(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getComponentsTool.name: {
                        const request = args as unknown as GetComponentsRequest;
                        const result = await getComponents(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getComponentDetailTool.name: {
                        const request = args as unknown as GetComponentDetailRequest;
                        const result = await getComponentDetail(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getFeatureStatusesTool.name: {
                        const request = args as unknown as GetFeatureStatusesRequest;
                        const result = await getFeatureStatuses(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getNotesTool.name: {
                        const request = args as unknown as GetNotesRequest;
                        const result = await getNotes(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getNoteDetailTool.name: {
                        const request = args as unknown as GetNoteDetailRequest;
                        const result = await getNoteDetail(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getCompaniesTool.name: {
                        const request = args as unknown as GetCompaniesRequest;
                        const result = await getCompanies(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    case getCompanyDetailTool.name: {
                        const request = args as unknown as GetCompanyDetailRequest;
                        const result = await getCompanyDetail(request);
                        return {
                            content: [{ type: "text", text: JSON.stringify(result) }],
                        }
                    }

                    default:
                        throw new Error(`Unknown tool: ${name}`);
                }

            } catch (error) {
                console.error("Error executing tool: ", error);
                return {
                    content: [
                        {
                            type: "text",
                            text: JSON.stringify({
                                error: error instanceof Error ? error.message : String(error),
                            }),
                        },
                    ],
                };
            }
        }
    )

    server.setRequestHandler(ListToolsRequestSchema, async () => {
        console.info("Received ListToolsRequest");
        return {
            tools: [
                getProductsTool,
                getProductDetailTool,
                getFeaturesTool,
                getFeatureDetailTool,
                getComponentsTool,
                getComponentDetailTool,
                getFeatureStatusesTool,
                getNotesTool,
                getNoteDetailTool,
                getCompaniesTool,
                getCompanyDetailTool
            ],
        };
    });

    const transport = new StdioServerTransport();
    console.log("Connecting server to transport...");
    await server.connect(transport);

    console.log("Productboard MCP Server running on stdio");
}

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

```