pinpointmcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # Pinpoint MCP Server
2
+
3
+ MCP server for the [Pinpoint](https://testwithpinpoint.com) bug tracking platform. Enables AI agents in Claude Code, Cursor, and other MCP-compatible environments to list bugs, inspect details, update status, and autonomously resolve reported issues.
4
+
5
+ ## Quick Start
6
+
7
+ ### Install from npm
8
+
9
+ ```bash
10
+ npx -y pinpointmcp
11
+ ```
12
+
13
+ That's it. The server starts without any pre-configuration and exposes a `configure` tool so your AI agent can set up credentials interactively. You can also pre-configure via environment variables or a dotfile (see [Configuration](#configuration) below).
14
+
15
+ ### Build from source
16
+
17
+ ```bash
18
+ cd code/mcp
19
+ npm install
20
+ npm run build
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ The server resolves credentials in this order:
26
+
27
+ 1. **Environment variables** (highest precedence)
28
+ 2. **Dotfile** at `~/.pinpoint/config.json`
29
+ 3. **Unconfigured** (server starts, all tools return setup guidance until configured)
30
+
31
+ ### Option 1: Environment Variables
32
+
33
+ | Variable | Required | Default | Description |
34
+ |----------|----------|---------|-------------|
35
+ | `PINPOINT_TOKEN` | No | n/a | API authentication token |
36
+ | `PINPOINT_API_URL` | No | `https://api.testwithpinpoint.com` | API base URL |
37
+
38
+ ```bash
39
+ export PINPOINT_TOKEN=your-token-here
40
+ ```
41
+
42
+ ### Option 2: Interactive Configuration (First-Boot)
43
+
44
+ Launch the server without any token set. The agent can then call the `configure` tool:
45
+
46
+ ```json
47
+ { "token": "your-pinpoint-api-token" }
48
+ ```
49
+
50
+ The server validates the token against the API, swaps in a live client, and persists the credentials to `~/.pinpoint/config.json` (with `0600` file permissions) for future sessions.
51
+
52
+ ### Option 3: Dotfile
53
+
54
+ Create `~/.pinpoint/config.json` manually:
55
+
56
+ ```json
57
+ {
58
+ "token": "your-pinpoint-api-token",
59
+ "apiUrl": "https://api.testwithpinpoint.com"
60
+ }
61
+ ```
62
+
63
+ ### Claude Code / Cursor / Windsurf
64
+
65
+ Add to your project's `.mcp.json` (or the equivalent for your platform):
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "pinpoint": {
71
+ "command": "npx",
72
+ "args": ["-y", "pinpointmcp"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ If you have a token ready, you can pass it as an environment variable to skip the interactive setup:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "pinpoint": {
84
+ "command": "npx",
85
+ "args": ["-y", "pinpointmcp"],
86
+ "env": {
87
+ "PINPOINT_TOKEN": "your-token-here"
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Claude Desktop
95
+
96
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "pinpoint": {
102
+ "command": "npx",
103
+ "args": ["-y", "pinpointmcp"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Tools
110
+
111
+ ### configure
112
+
113
+ Configure the server with an API token at runtime. Validates the token before persisting.
114
+
115
+ | Parameter | Type | Required | Default | Description |
116
+ |-----------|------|----------|---------|-------------|
117
+ | `token` | string | Yes | n/a | Your Pinpoint API token |
118
+ | `api_url` | string | No | `https://api.testwithpinpoint.com` | API base URL for self-hosted instances |
119
+
120
+ **Behavior:**
121
+ - Validates the token by making a lightweight API call
122
+ - On success: swaps the live client, persists to `~/.pinpoint/config.json`, returns confirmation
123
+ - On validation failure: returns an error without persisting
124
+ - On write failure: configures the current session and warns about persistence
125
+
126
+ ### list_bugs
127
+
128
+ List bug reports filtered by status and project.
129
+
130
+ | Parameter | Type | Required | Default | Description |
131
+ |-----------|------|----------|---------|-------------|
132
+ | `status` | string | No | `"open"` | Filter: `open`, `in_progress`, `resolved`, `closed` |
133
+ | `project` | string | No | n/a | Filter by project name |
134
+ | `page` | number | No | `0` | Page number (0-indexed) |
135
+ | `size` | number | No | `20` | Page size (max 200) |
136
+
137
+ ### get_bug
138
+
139
+ Get detailed information about a specific bug report, including description, reproduction steps, expected/actual behavior, and environment.
140
+
141
+ | Parameter | Type | Required | Description |
142
+ |-----------|------|----------|-------------|
143
+ | `id` | string | Yes | Bug report UUID |
144
+
145
+ ### update_bug_status
146
+
147
+ Update the status of a bug report.
148
+
149
+ | Parameter | Type | Required | Description |
150
+ |-----------|------|----------|-------------|
151
+ | `id` | string | Yes | Bug report UUID |
152
+ | `status` | enum | Yes | New status: `open`, `in_progress`, `resolved`, `closed` |
153
+ | `resolution` | string | No | Resolution notes (recommended when resolving) |
154
+
155
+ ## Resources
156
+
157
+ | URI | Format | Description |
158
+ |-----|--------|-------------|
159
+ | `pinpoint://bugs` | JSON | All open bugs as a JSON array |
160
+ | `pinpoint://bugs/{id}` | Markdown | Detailed bug report rendered as Markdown |
161
+
162
+ ## Prompts
163
+
164
+ ### solve_bug
165
+
166
+ Structured prompt for analyzing and fixing a specific bug. Assembles the title, description, reproduction steps, expected/actual behavior, and environment into a context block, then asks the agent to identify the root cause, implement a fix, and create a merge request.
167
+
168
+ | Argument | Type | Required | Description |
169
+ |----------|------|----------|-------------|
170
+ | `bug_id` | string | Yes | UUID of the bug to solve |
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ npm run dev # Watch mode (recompiles on save)
176
+ npm run build # Compile TypeScript to dist/
177
+ npm test # Run all tests
178
+ npm start # Start the server on stdio
179
+ ```
180
+
181
+ ### Test Suite
182
+
183
+ 77 tests across 10 files:
184
+
185
+ | File | Tests | Coverage |
186
+ |------|-------|----------|
187
+ | `client.test.ts` | 16 | HTTP client behavior, URL construction, headers, error hierarchy |
188
+ | `client-holder.test.ts` | 7 | Mutable client wrapper: configured/unconfigured states, swap |
189
+ | `config.test.ts` | 6 | Config precedence: env var > dotfile > unconfigured |
190
+ | `config-store.test.ts` | 7 | Dotfile read/write, permissions, invalid JSON handling |
191
+ | `configure-tool.test.ts` | 6 | Token validation, persistence, failure modes, truncation |
192
+ | `setup-message.test.ts` | 5 | Setup guidance text for tools, resources, prompts |
193
+ | `tools.test.ts` | 11 | Tool output formatting, pagination, error wrapping, unconfigured guard |
194
+ | `resources.test.ts` | 8 | Static and templated resources, array variables, unconfigured guard |
195
+ | `prompts.test.ts` | 3 | Prompt assembly, null section omission, unconfigured guard |
196
+ | `integration.test.ts` | 8 | Full MCP handshake via `InMemoryTransport`, schema validation |
197
+
198
+ **Coverage:** 100% statements, 100% lines, 100% functions, 93% branches.
199
+
200
+ ```bash
201
+ npx vitest run --coverage # Run with coverage report
202
+ ```
203
+
204
+ ## Architecture
205
+
206
+ ```
207
+ src/
208
+ index.ts Entry point: loads config, creates holder, registers handlers
209
+ config.ts Async config loader (env var > dotfile > unconfigured)
210
+ config-store.ts ~/.pinpoint/config.json read/write with 0600 permissions
211
+ client.ts PinpointClient HTTP client for the Pinpoint API
212
+ client-holder.ts Mutable wrapper so the client can be swapped at runtime
213
+ setup-message.ts Shared setup guidance for unconfigured state
214
+ types.ts TypeScript types and error class hierarchy
215
+ tools/
216
+ configure.ts configure tool (token validation + persistence)
217
+ list-bugs.ts list_bugs tool handler
218
+ get-bug.ts get_bug tool handler
219
+ update-bug-status.ts update_bug_status tool handler
220
+ index.ts Barrel export
221
+ resources/
222
+ bugs.ts Static bug list and templated bug detail resources
223
+ index.ts Barrel export
224
+ prompts/
225
+ solve-bug.ts solve_bug prompt handler
226
+ index.ts Barrel export
227
+ __tests__/
228
+ client.test.ts
229
+ client-holder.test.ts
230
+ config.test.ts
231
+ config-store.test.ts
232
+ configure-tool.test.ts
233
+ setup-message.test.ts
234
+ tools.test.ts
235
+ resources.test.ts
236
+ prompts.test.ts
237
+ integration.test.ts
238
+ ```
239
+
240
+ ### First-Boot Flow
241
+
242
+ When no token is available (no env var, no dotfile), the server starts in an unconfigured state:
243
+
244
+ ```
245
+ ┌───────────────┐ ┌──────────────┐ ┌───────────────────┐
246
+ │ loadConfig() │────>│ ClientHolder │────>│ McpServer starts │
247
+ │ token = null │ │ client = null│ │ on stdio transport │
248
+ └───────────────┘ └──────────────┘ └───────────────────┘
249
+
250
+ ┌────────────────────────┴──────────────────────┐
251
+ │ │
252
+ Agent calls any tool Agent calls "configure"
253
+ (list_bugs, get_bug, ...) with { token: "..." }
254
+ │ │
255
+ Returns setup guidance Validates token via API
256
+ with instructions to Swaps client on holder
257
+ call "configure" Persists to dotfile
258
+ Returns confirmation
259
+
260
+ All tools now work normally
261
+ ```
262
+
263
+ ## Troubleshooting
264
+
265
+ - **Server starts but tools return setup guidance:** No token is configured. Either set `PINPOINT_TOKEN`, create the dotfile, or ask the agent to call the `configure` tool.
266
+ - **"Token validation failed":** The token was rejected by the API. Verify it is valid and has not expired. Check the Pinpoint dashboard under Settings > API Tokens.
267
+ - **"Could not save configuration to disk":** The server configured successfully for this session but could not write `~/.pinpoint/config.json`. Check directory permissions on `~/.pinpoint/`.
268
+ - **"Authentication failed" on tool calls:** Your stored or environment token may have expired. Re-run the `configure` tool with a fresh token.
269
+ - **Connection errors:** Check `PINPOINT_API_URL` and confirm network connectivity to the API.
270
+ - **Server not discovered by Claude Code:** Ensure `.mcp.json` exists in the project root and that `npm run build` has been executed so `dist/index.js` is present.
@@ -0,0 +1,8 @@
1
+ import { PinpointClient } from "./client.js";
2
+ export declare class ClientHolder {
3
+ private client;
4
+ constructor(client?: PinpointClient | null);
5
+ isConfigured(): boolean;
6
+ getClient(): PinpointClient;
7
+ setClient(client: PinpointClient): void;
8
+ }
@@ -0,0 +1,18 @@
1
+ export class ClientHolder {
2
+ client;
3
+ constructor(client = null) {
4
+ this.client = client;
5
+ }
6
+ isConfigured() {
7
+ return this.client !== null;
8
+ }
9
+ getClient() {
10
+ if (!this.client) {
11
+ throw new Error("Pinpoint client is not configured. Call the configure tool first.");
12
+ }
13
+ return this.client;
14
+ }
15
+ setClient(client) {
16
+ this.client = client;
17
+ }
18
+ }
@@ -0,0 +1,10 @@
1
+ import { BugReport, PaginatedResponse, ListBugsOptions } from "./types.js";
2
+ export declare class PinpointClient {
3
+ private baseUrl;
4
+ private token;
5
+ constructor(baseUrl: string, token: string);
6
+ listBugs(options?: ListBugsOptions): Promise<PaginatedResponse<BugReport>>;
7
+ getBug(id: string): Promise<BugReport>;
8
+ updateBugStatus(id: string, status: string, resolution?: string): Promise<BugReport>;
9
+ private request;
10
+ }
package/dist/client.js ADDED
@@ -0,0 +1,63 @@
1
+ import { AuthenticationError, NotFoundError, ServerError, ConnectionError, } from "./types.js";
2
+ export class PinpointClient {
3
+ baseUrl;
4
+ token;
5
+ constructor(baseUrl, token) {
6
+ this.baseUrl = baseUrl.replace(/\/$/, "");
7
+ this.token = token;
8
+ }
9
+ async listBugs(options) {
10
+ const params = new URLSearchParams();
11
+ params.set("status", options?.status ?? "open");
12
+ if (options?.project)
13
+ params.set("project", options.project);
14
+ params.set("page", String(options?.page ?? 0));
15
+ params.set("size", String(options?.size ?? 20));
16
+ return this.request(`/api/v1/bugs?${params.toString()}`);
17
+ }
18
+ async getBug(id) {
19
+ return this.request(`/api/v1/bugs/${encodeURIComponent(id)}`);
20
+ }
21
+ async updateBugStatus(id, status, resolution) {
22
+ const body = { status };
23
+ if (resolution !== undefined) {
24
+ body.resolution = resolution;
25
+ }
26
+ return this.request(`/api/v1/bugs/${encodeURIComponent(id)}/status`, {
27
+ method: "PATCH",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify(body),
30
+ });
31
+ }
32
+ async request(path, init) {
33
+ const url = `${this.baseUrl}${path}`;
34
+ let response;
35
+ try {
36
+ response = await fetch(url, {
37
+ ...init,
38
+ headers: {
39
+ Authorization: `Bearer ${this.token}`,
40
+ Accept: "application/json",
41
+ ...init?.headers,
42
+ },
43
+ });
44
+ }
45
+ catch {
46
+ throw new ConnectionError(this.baseUrl);
47
+ }
48
+ if (!response.ok) {
49
+ switch (response.status) {
50
+ case 401:
51
+ throw new AuthenticationError();
52
+ case 404:
53
+ throw new NotFoundError();
54
+ default:
55
+ if (response.status >= 500) {
56
+ throw new ServerError(`Server error: ${response.status} ${response.statusText}`, response.status);
57
+ }
58
+ throw new ServerError(`Request failed: ${response.status} ${response.statusText}`, response.status);
59
+ }
60
+ }
61
+ return response.json();
62
+ }
63
+ }
@@ -0,0 +1,7 @@
1
+ export interface StoredConfig {
2
+ token: string;
3
+ apiUrl: string;
4
+ }
5
+ export declare function getConfigPath(): string;
6
+ export declare function readStoredConfig(): Promise<StoredConfig | null>;
7
+ export declare function writeStoredConfig(config: StoredConfig): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function getConfigPath() {
5
+ return join(homedir(), ".pinpoint", "config.json");
6
+ }
7
+ export async function readStoredConfig() {
8
+ try {
9
+ const raw = await readFile(getConfigPath(), "utf-8");
10
+ const parsed = JSON.parse(raw);
11
+ if (typeof parsed.token === "string" && typeof parsed.apiUrl === "string") {
12
+ return { token: parsed.token, apiUrl: parsed.apiUrl };
13
+ }
14
+ return null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export async function writeStoredConfig(config) {
21
+ const configPath = getConfigPath();
22
+ const dir = join(homedir(), ".pinpoint");
23
+ await mkdir(dir, { recursive: true });
24
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", {
25
+ mode: 0o600,
26
+ });
27
+ }
@@ -0,0 +1,5 @@
1
+ export interface PinpointConfig {
2
+ token: string | null;
3
+ apiUrl: string;
4
+ }
5
+ export declare function loadConfig(): Promise<PinpointConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,25 @@
1
+ import { readStoredConfig } from "./config-store.js";
2
+ export async function loadConfig() {
3
+ // Env var takes highest precedence
4
+ const envToken = process.env.PINPOINT_TOKEN ?? null;
5
+ const envUrl = process.env.PINPOINT_API_URL;
6
+ if (envToken) {
7
+ return {
8
+ token: envToken,
9
+ apiUrl: envUrl || "https://api.testwithpinpoint.com",
10
+ };
11
+ }
12
+ // Fall back to dotfile
13
+ const stored = await readStoredConfig();
14
+ if (stored) {
15
+ return {
16
+ token: stored.token,
17
+ apiUrl: envUrl || stored.apiUrl,
18
+ };
19
+ }
20
+ // Start unconfigured
21
+ return {
22
+ token: null,
23
+ apiUrl: envUrl || "https://api.testwithpinpoint.com",
24
+ };
25
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { loadConfig } from "./config.js";
5
+ import { PinpointClient } from "./client.js";
6
+ import { ClientHolder } from "./client-holder.js";
7
+ import { registerConfigureTool, registerListBugsTool, registerGetBugTool, registerUpdateBugStatusTool } from "./tools/index.js";
8
+ import { registerBugListResource, registerBugDetailResource } from "./resources/index.js";
9
+ import { registerSolveBugPrompt } from "./prompts/index.js";
10
+ async function main() {
11
+ const config = await loadConfig();
12
+ const holder = new ClientHolder(config.token ? new PinpointClient(config.apiUrl, config.token) : null);
13
+ const server = new McpServer({
14
+ name: "pinpoint",
15
+ version: "0.1.0",
16
+ });
17
+ // Register configure tool first (always available)
18
+ registerConfigureTool(server, holder);
19
+ // Register bug tools, resources, and prompts
20
+ registerListBugsTool(server, holder);
21
+ registerGetBugTool(server, holder);
22
+ registerUpdateBugStatusTool(server, holder);
23
+ registerBugListResource(server, holder);
24
+ registerBugDetailResource(server, holder);
25
+ registerSolveBugPrompt(server, holder);
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+ if (holder.isConfigured()) {
29
+ console.error("Pinpoint MCP server running on stdio");
30
+ }
31
+ else {
32
+ console.error("Pinpoint MCP server running on stdio (unconfigured, use the configure tool to set your API token)");
33
+ }
34
+ }
35
+ main().catch(console.error);
@@ -0,0 +1 @@
1
+ export { registerSolveBugPrompt } from "./solve-bug.js";
@@ -0,0 +1 @@
1
+ export { registerSolveBugPrompt } from "./solve-bug.js";
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ClientHolder } from "../client-holder.js";
3
+ export declare function registerSolveBugPrompt(server: McpServer, holder: ClientHolder): void;
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { unconfiguredPromptResult } from "../setup-message.js";
3
+ export function registerSolveBugPrompt(server, holder) {
4
+ server.prompt("solve_bug", "Structured prompt for analyzing and fixing a specific bug", { bug_id: z.string().describe("UUID of the bug to solve") }, async ({ bug_id }) => {
5
+ if (!holder.isConfigured())
6
+ return unconfiguredPromptResult();
7
+ const bug = await holder.getClient().getBug(bug_id);
8
+ const text = [
9
+ `Please help me fix the following bug:\n`,
10
+ `## ${bug.title}`,
11
+ `**Severity:** ${bug.severity} | **Component:** ${bug.component || "Unknown"}`,
12
+ bug.description ? `\n### Description\n${bug.description}` : "",
13
+ bug.stepsToReproduce ? `\n### Steps to Reproduce\n${bug.stepsToReproduce}` : "",
14
+ bug.expectedBehavior ? `\n### Expected Behavior\n${bug.expectedBehavior}` : "",
15
+ bug.actualBehavior ? `\n### Actual Behavior\n${bug.actualBehavior}` : "",
16
+ bug.environment ? `\n### Environment\n${bug.environment}` : "",
17
+ `\nPlease analyze the bug, identify the root cause, implement a fix, and create a merge request.`,
18
+ ].filter(Boolean).join("\n");
19
+ return {
20
+ messages: [{
21
+ role: "user",
22
+ content: {
23
+ type: "text",
24
+ text,
25
+ },
26
+ }],
27
+ };
28
+ });
29
+ }
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ClientHolder } from "../client-holder.js";
3
+ export declare function registerBugListResource(server: McpServer, holder: ClientHolder): void;
4
+ export declare function registerBugDetailResource(server: McpServer, holder: ClientHolder): void;
@@ -0,0 +1,43 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { unconfiguredResourceResult } from "../setup-message.js";
3
+ export function registerBugListResource(server, holder) {
4
+ server.resource("bug-list", "pinpoint://bugs", async (uri) => {
5
+ if (!holder.isConfigured())
6
+ return unconfiguredResourceResult(uri.href);
7
+ const result = await holder.getClient().listBugs({ status: "open" });
8
+ return {
9
+ contents: [{
10
+ uri: uri.href,
11
+ mimeType: "application/json",
12
+ text: JSON.stringify(result.content, null, 2),
13
+ }],
14
+ };
15
+ });
16
+ }
17
+ export function registerBugDetailResource(server, holder) {
18
+ server.resource("bug-detail", new ResourceTemplate("pinpoint://bugs/{id}", { list: undefined }), async (uri, variables) => {
19
+ if (!holder.isConfigured())
20
+ return unconfiguredResourceResult(uri.href);
21
+ const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
22
+ const bug = await holder.getClient().getBug(id);
23
+ const markdown = [
24
+ `# ${bug.title}`,
25
+ `**Severity:** ${bug.severity} | **Status:** ${bug.status} | **Component:** ${bug.component || "N/A"}`,
26
+ bug.description ? `## Description\n${bug.description}` : "",
27
+ bug.stepsToReproduce ? `## Steps to Reproduce\n${bug.stepsToReproduce}` : "",
28
+ bug.expectedBehavior ? `## Expected Behavior\n${bug.expectedBehavior}` : "",
29
+ bug.actualBehavior ? `## Actual Behavior\n${bug.actualBehavior}` : "",
30
+ bug.environment ? `## Environment\n${bug.environment}` : "",
31
+ bug.rootCause ? `## Root Cause\n${bug.rootCause}` : "",
32
+ bug.resolution ? `## Resolution\n${bug.resolution}` : "",
33
+ `\n**Reporter:** ${bug.reporterName} | **Created:** ${bug.createdAt}`,
34
+ ].filter(Boolean).join("\n\n");
35
+ return {
36
+ contents: [{
37
+ uri: uri.href,
38
+ mimeType: "text/markdown",
39
+ text: markdown,
40
+ }],
41
+ };
42
+ });
43
+ }
@@ -0,0 +1 @@
1
+ export { registerBugListResource, registerBugDetailResource } from "./bugs.js";
@@ -0,0 +1 @@
1
+ export { registerBugListResource, registerBugDetailResource } from "./bugs.js";
@@ -0,0 +1,24 @@
1
+ export declare function getSetupText(): string;
2
+ export declare function unconfiguredToolResult(): {
3
+ content: Array<{
4
+ type: "text";
5
+ text: string;
6
+ }>;
7
+ isError: true;
8
+ };
9
+ export declare function unconfiguredResourceResult(uri: string): {
10
+ contents: Array<{
11
+ uri: string;
12
+ mimeType: string;
13
+ text: string;
14
+ }>;
15
+ };
16
+ export declare function unconfiguredPromptResult(): {
17
+ messages: Array<{
18
+ role: "user";
19
+ content: {
20
+ type: "text";
21
+ text: string;
22
+ };
23
+ }>;
24
+ };
@@ -0,0 +1,34 @@
1
+ const SETUP_TEXT = [
2
+ "Pinpoint MCP server is not yet configured.",
3
+ "",
4
+ "To get started, call the `configure` tool with your API token:",
5
+ "",
6
+ ' { "token": "your-pinpoint-api-token" }',
7
+ "",
8
+ "You can find your token in the Pinpoint dashboard under Settings > API Tokens.",
9
+ "Optionally pass `api_url` if you use a self-hosted instance.",
10
+ ].join("\n");
11
+ export function getSetupText() {
12
+ return SETUP_TEXT;
13
+ }
14
+ export function unconfiguredToolResult() {
15
+ return {
16
+ content: [{ type: "text", text: SETUP_TEXT }],
17
+ isError: true,
18
+ };
19
+ }
20
+ export function unconfiguredResourceResult(uri) {
21
+ return {
22
+ contents: [{ uri, mimeType: "text/plain", text: SETUP_TEXT }],
23
+ };
24
+ }
25
+ export function unconfiguredPromptResult() {
26
+ return {
27
+ messages: [
28
+ {
29
+ role: "user",
30
+ content: { type: "text", text: SETUP_TEXT },
31
+ },
32
+ ],
33
+ };
34
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ClientHolder } from "../client-holder.js";
3
+ export declare function registerConfigureTool(server: McpServer, holder: ClientHolder): void;
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { PinpointClient } from "../client.js";
3
+ import { writeStoredConfig } from "../config-store.js";
4
+ const DEFAULT_API_URL = "https://api.testwithpinpoint.com";
5
+ export function registerConfigureTool(server, holder) {
6
+ server.tool("configure", "Configure the Pinpoint MCP server with your API token", {
7
+ token: z.string().describe("Your Pinpoint API token"),
8
+ api_url: z.string().optional().describe("Pinpoint API URL (defaults to production)"),
9
+ }, async (params) => {
10
+ const apiUrl = params.api_url || DEFAULT_API_URL;
11
+ const client = new PinpointClient(apiUrl, params.token);
12
+ // Validate the token with a lightweight call
13
+ try {
14
+ await client.listBugs({ size: 1 });
15
+ }
16
+ catch (error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return {
19
+ content: [{ type: "text", text: `Token validation failed: ${message}\n\nPlease check your token and try again.` }],
20
+ isError: true,
21
+ };
22
+ }
23
+ // Token is valid, swap the client
24
+ holder.setClient(client);
25
+ // Persist to dotfile
26
+ let persisted = true;
27
+ try {
28
+ await writeStoredConfig({ token: params.token, apiUrl });
29
+ }
30
+ catch {
31
+ persisted = false;
32
+ }
33
+ const lines = [
34
+ "Pinpoint MCP server configured successfully!",
35
+ "",
36
+ `API URL: ${apiUrl}`,
37
+ `Token: ${params.token.slice(0, 8)}...`,
38
+ ];
39
+ if (persisted) {
40
+ lines.push("", "Configuration saved to ~/.pinpoint/config.json for future sessions.");
41
+ }
42
+ else {
43
+ lines.push("", "Warning: Could not save configuration to disk. The token will only be available for this session.");
44
+ }
45
+ return {
46
+ content: [{ type: "text", text: lines.join("\n") }],
47
+ };
48
+ });
49
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ClientHolder } from "../client-holder.js";
3
+ export declare function registerGetBugTool(server: McpServer, holder: ClientHolder): void;
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { unconfiguredToolResult } from "../setup-message.js";
3
+ export function registerGetBugTool(server, holder) {
4
+ server.tool("get_bug", "Get detailed information about a specific bug report", {
5
+ id: z.string().describe("Bug report UUID"),
6
+ }, async (params) => {
7
+ if (!holder.isConfigured())
8
+ return unconfiguredToolResult();
9
+ try {
10
+ const bug = await holder.getClient().getBug(params.id);
11
+ const sections = [
12
+ `# ${bug.title}`,
13
+ `**Severity:** ${bug.severity} | **Status:** ${bug.status} | **Component:** ${bug.component || "N/A"}`,
14
+ bug.description ? `## Description\n${bug.description}` : null,
15
+ bug.stepsToReproduce ? `## Steps to Reproduce\n${bug.stepsToReproduce}` : null,
16
+ bug.expectedBehavior ? `## Expected Behavior\n${bug.expectedBehavior}` : null,
17
+ bug.actualBehavior ? `## Actual Behavior\n${bug.actualBehavior}` : null,
18
+ bug.environment ? `## Environment\n${bug.environment}` : null,
19
+ bug.rootCause ? `## Root Cause\n${bug.rootCause}` : null,
20
+ bug.resolution ? `## Resolution\n${bug.resolution}` : null,
21
+ `\n**Reporter:** ${bug.reporterName} | **Created:** ${bug.createdAt}`,
22
+ ].filter(Boolean).join("\n\n");
23
+ return { content: [{ type: "text", text: sections }] };
24
+ }
25
+ catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ return { content: [{ type: "text", text: `Error fetching bug: ${message}` }], isError: true };
28
+ }
29
+ });
30
+ }
@@ -0,0 +1,4 @@
1
+ export { registerListBugsTool } from "./list-bugs.js";
2
+ export { registerGetBugTool } from "./get-bug.js";
3
+ export { registerUpdateBugStatusTool } from "./update-bug-status.js";
4
+ export { registerConfigureTool } from "./configure.js";
@@ -0,0 +1,4 @@
1
+ export { registerListBugsTool } from "./list-bugs.js";
2
+ export { registerGetBugTool } from "./get-bug.js";
3
+ export { registerUpdateBugStatusTool } from "./update-bug-status.js";
4
+ export { registerConfigureTool } from "./configure.js";
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ClientHolder } from "../client-holder.js";
3
+ export declare function registerListBugsTool(server: McpServer, holder: ClientHolder): void;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { unconfiguredToolResult } from "../setup-message.js";
3
+ export function registerListBugsTool(server, holder) {
4
+ server.tool("list_bugs", "List bug reports filtered by status and project", {
5
+ status: z.string().optional().default("open").describe("Filter by bug status (open, in_progress, resolved, closed)"),
6
+ project: z.string().optional().describe("Filter by project name"),
7
+ page: z.number().optional().default(0).describe("Page number (0-indexed)"),
8
+ size: z.number().optional().default(20).describe("Page size (max 200)"),
9
+ }, async (params) => {
10
+ if (!holder.isConfigured())
11
+ return unconfiguredToolResult();
12
+ try {
13
+ const result = await holder.getClient().listBugs(params);
14
+ const formatted = result.content.map(bug => `[${bug.severity.toUpperCase()}] ${bug.title} (${bug.id})\n Status: ${bug.status} | Component: ${bug.component || "N/A"}`).join("\n\n");
15
+ const summary = `Found ${result.totalElements} bugs (page ${result.page + 1} of ${result.totalPages})`;
16
+ return { content: [{ type: "text", text: `${summary}\n\n${formatted}` }] };
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return { content: [{ type: "text", text: `Error listing bugs: ${message}` }], isError: true };
21
+ }
22
+ });
23
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ClientHolder } from "../client-holder.js";
3
+ export declare function registerUpdateBugStatusTool(server: McpServer, holder: ClientHolder): void;
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { unconfiguredToolResult } from "../setup-message.js";
3
+ export function registerUpdateBugStatusTool(server, holder) {
4
+ server.tool("update_bug_status", "Update the status of a bug report", {
5
+ id: z.string().describe("Bug report UUID"),
6
+ status: z.enum(["open", "in_progress", "resolved", "closed"]).describe("New status"),
7
+ resolution: z.string().optional().describe("Resolution notes (recommended when setting status to resolved)"),
8
+ }, async (params) => {
9
+ if (!holder.isConfigured())
10
+ return unconfiguredToolResult();
11
+ try {
12
+ const bug = await holder.getClient().updateBugStatus(params.id, params.status, params.resolution);
13
+ return {
14
+ content: [{
15
+ type: "text",
16
+ text: `Bug "${bug.title}" status updated to ${bug.status}${bug.resolution ? `\nResolution: ${bug.resolution}` : ""}`,
17
+ }],
18
+ };
19
+ }
20
+ catch (error) {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ return { content: [{ type: "text", text: `Error updating bug status: ${message}` }], isError: true };
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,54 @@
1
+ export interface BugReport {
2
+ id: string;
3
+ dispatchEventId: string | null;
4
+ reporterName: string;
5
+ title: string;
6
+ severity: string;
7
+ status: string;
8
+ component: string | null;
9
+ description: string | null;
10
+ stepsToReproduce: string | null;
11
+ expectedBehavior: string | null;
12
+ actualBehavior: string | null;
13
+ environment: string | null;
14
+ rootCause: string | null;
15
+ resolution: string | null;
16
+ fileName: string | null;
17
+ fileSize: number | null;
18
+ downloadUrl: string | null;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+ export interface PaginatedResponse<T> {
23
+ content: T[];
24
+ page: number;
25
+ size: number;
26
+ totalElements: number;
27
+ totalPages: number;
28
+ }
29
+ export interface ListBugsOptions {
30
+ status?: string;
31
+ project?: string;
32
+ page?: number;
33
+ size?: number;
34
+ }
35
+ export interface UpdateBugStatusRequest {
36
+ status: string;
37
+ resolution?: string;
38
+ }
39
+ export declare class PinpointError extends Error {
40
+ statusCode?: number | undefined;
41
+ constructor(message: string, statusCode?: number | undefined);
42
+ }
43
+ export declare class AuthenticationError extends PinpointError {
44
+ constructor(message?: string);
45
+ }
46
+ export declare class NotFoundError extends PinpointError {
47
+ constructor(message?: string);
48
+ }
49
+ export declare class ServerError extends PinpointError {
50
+ constructor(message?: string, statusCode?: number);
51
+ }
52
+ export declare class ConnectionError extends PinpointError {
53
+ constructor(url: string);
54
+ }
package/dist/types.js ADDED
@@ -0,0 +1,33 @@
1
+ // Error hierarchy
2
+ export class PinpointError extends Error {
3
+ statusCode;
4
+ constructor(message, statusCode) {
5
+ super(message);
6
+ this.statusCode = statusCode;
7
+ this.name = "PinpointError";
8
+ }
9
+ }
10
+ export class AuthenticationError extends PinpointError {
11
+ constructor(message = "Authentication failed. Check your API token.") {
12
+ super(message, 401);
13
+ this.name = "AuthenticationError";
14
+ }
15
+ }
16
+ export class NotFoundError extends PinpointError {
17
+ constructor(message = "Resource not found.") {
18
+ super(message, 404);
19
+ this.name = "NotFoundError";
20
+ }
21
+ }
22
+ export class ServerError extends PinpointError {
23
+ constructor(message = "Server error", statusCode = 500) {
24
+ super(message, statusCode);
25
+ this.name = "ServerError";
26
+ }
27
+ }
28
+ export class ConnectionError extends PinpointError {
29
+ constructor(url) {
30
+ super(`Could not connect to ${url}`);
31
+ this.name = "ConnectionError";
32
+ }
33
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "pinpointmcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Pinpoint bug tracking. Enables AI agents to list, inspect, and resolve bugs.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "pinpointmcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc --watch",
18
+ "start": "node dist/index.js",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "prepublishOnly": "npm run build && npm test"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "pinpoint",
27
+ "bug-tracking",
28
+ "ai-agent",
29
+ "claude",
30
+ "testing"
31
+ ],
32
+ "author": "Pinpoint",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/testwithpinpoint/pinpoint",
37
+ "directory": "code/mcp"
38
+ },
39
+ "homepage": "https://testwithpinpoint.com",
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.27.1",
45
+ "zod": "^4.3.6"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.3.2",
49
+ "@vitest/coverage-v8": "^4.0.18",
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^4.0.18"
52
+ }
53
+ }