Skip to content

Conversation

trknhr
Copy link
Collaborator

@trknhr trknhr commented Oct 7, 2025

This update introduces a robust Project Access Control feature to enhance security by restricting the server's operations to an allow-list of Backlog projects. It also includes significant improvements to the development workflow and configuration management.

Key Features & Enhancements

🛡️ Project Access Guards

  • Restricted Project Access: You can now define a specific list of Backlog projects that this server instance is allowed to interact with. Any tool call targeting a project not on this list will be blocked.
  • Configuration: The allow-list is configured via new environment variables:
    • BACKLOG_ALLOWED_PROJECT_IDS: A comma-separated list of project IDs.
    • BACKLOG_ALLOWED_PROJECT_KEYS: A comma-separated list of project keys (e.g., "BLG", "MYPROJ"). These keys are resolved to their corresponding IDs at startup.
  • New Service and Error Handling:
    • A ProjectGuardService has been implemented to manage and enforce these access rules.
    • A new custom error, ProjectAccessForbiddenError, is thrown when access is denied, providing clear, structured error data in the MCP response.
  • Seamless Integration: The guard logic is applied transparently to all tool handlers using a wrapper function, ensuring consistent security without modifying each tool individually.

⚙️ Developer Experience & Refactoring

  • Faster Development Workflow: The dev script has been updated from ts-node to tsx. This provides significantly faster startup and hot-reloading capabilities, improving the development feedback loop.
  • Centralized Configuration: All configuration logic (using yargs and env-var) has been consolidated from index.ts into a new, dedicated src/config.ts module. This makes configuration clearer and easier to manage.

@trknhr trknhr requested a review from Copilot October 7, 2025 04:55
@trknhr trknhr self-assigned this Oct 7, 2025
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a comprehensive project access control system for the Backlog MCP server to enhance security by restricting operations to an allow-list of projects. The implementation includes guard services, error handling, and configuration management improvements.

  • Adds ProjectGuardService to enforce project access restrictions with configurable read/write guard policies
  • Introduces ProjectAccessForbiddenError for structured error responses when access is denied
  • Centralizes configuration management from index.ts into a dedicated config.ts module

Reviewed Changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/config.ts New centralized configuration module with project access control options
src/guards/ProjectGuardService.ts Core service for managing and enforcing project access rules
src/handlers/transformers/wrapWithProjectGuard.ts Wrapper function to apply project guards to tool handlers
src/errors/ProjectAccessForbiddenError.ts Custom error class for project access violations
src/registerTools.ts Updated to integrate project guard wrapper with tool registration
src/index.ts Refactored to use centralized config and initialize guard service
src/types/result.ts Extended ErrorLike type to support error codes and data
src/backlog/backlogErrorHandler.ts Added handling for ProjectAccessForbiddenError
package.json Updated dev script to use tsx for faster development workflow

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

}
const allowed = candidates.find((id) => guardService.isAllowed(id));
return allowed ? [allowed] : [candidates[0]];
}
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the function definition. This will cause a syntax error.

Suggested change
}
};

Copilot uses AI. Check for mistakes.

targetProjectIds = [project.id];
}

for(const targetProjectId of targetProjectIds) {
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after 'for' keyword. Should be 'for (const targetProjectId of targetProjectIds)'.

Suggested change
for(const targetProjectId of targetProjectIds) {
for (const targetProjectId of targetProjectIds) {

Copilot uses AI. Check for mistakes.

// Copyright (c) 2025 Nulab inc.
// Licensed under the MIT License.

import { ReadGuardPolicy, WriteGuardPolicy } from '../config.js';
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imported types ReadGuardPolicy and WriteGuardPolicy are not used in this file. This import should be removed as it's unnecessary.

Suggested change
import { ReadGuardPolicy, WriteGuardPolicy } from '../config.js';

Copilot uses AI. Check for mistakes.

Copy link

@allenthich allenthich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trknhr Thanks for working on this and sharing the draft!
I was interested to see what type of solution you came up with and had a few comments that might be worth dropping in. Curious to hear what you think.

Comment on lines +61 to +65
registerTools(server, toolsetGroup, mcpOption, guardService, backlog);

// Register dynamic tool management tools if enabled
if (config.dynamicToolsets) {
const registrar = createToolRegistrar(server, toolsetGroup, mcpOption, guardService, backlog);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createToolRegistrar internally calls registerTools which means when using dynamicToolsets, registerTools is called twice with the same arguments. Could this call be cached and reused or are they different calls?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the helper function definitions isNilOrEmpty and coerceToNumericIds could be moved out of the function scope of wrapWithProjectGuard to improve the clarity a bit

return [];
}
const allowed = candidates.find((id) => guardService.isAllowed(id));
return allowed ? [allowed] : [candidates[0]];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still be choosing the first candidate if there are no allowed projectIds found?

Comment on lines +62 to +70
const resolveCandidate = (candidates: number[]): number[] => {
if (candidates.length === 0) {
return [];
}
const allowed = candidates.find((id) => guardService.isAllowed(id));
return allowed ? [allowed] : [candidates[0]];
}

let targetProjectIds: number[] = resolveCandidate(primaryCandidates);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might not need the helper function resolveCandidate:

Suggested change
const resolveCandidate = (candidates: number[]): number[] => {
if (candidates.length === 0) {
return [];
}
const allowed = candidates.find((id) => guardService.isAllowed(id));
return allowed ? [allowed] : [candidates[0]];
}
let targetProjectIds: number[] = resolveCandidate(primaryCandidates);
let targetProjectIds: number[] = primaryCandidates.filter(guardService.isAllowed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants