Skip to content

feat(skill): Trello MCP skill bundle (Epic 1) + client ergonomics - #84

Merged
delorenj merged 5 commits into
mainfrom
feat/trello-skill-bundle
May 9, 2026
Merged

delorenj merged 5 commits into
mainfrom
feat/trello-skill-bundle

Conversation

@delorenj

@delorenj delorenj commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Closes Epic 1 of the skill-package sprint: stories 1-1 through 1-4. Trello MCP can now be installed and consumed as a self-contained skill bundle.
  • Adds tooling tasks (mise package, mise ci) for bundle refresh and a combined test+refresh CI run.
  • Ships a small ergonomic improvement on updateChecklistItem so a bare state string (e.g. "complete") is accepted in addition to the full update object.

What's Included

Epic 1 stories

# Story Result
1-1 Initialize skill directory structure skill/assets/, skill/references/ scaffolded
1-2 Create SKILL.md workflow skill/SKILL.md authored as the skill entry point
1-3 Bundle install script skill/scripts/install.sh hardened; scripts/build-skill-assets.sh added to refresh the bundled source snapshot
1-4 Update documentation README.md updated for skill install and usage

Tooling

  • mise.toml: package task (refresh skill assets), ci task (npm test + asset refresh)
  • .gitignore: excludes local Codex state and Claude Flow data caches

Client ergonomics (small, included here)

  • src/trello-client.ts: updateChecklistItem now accepts a state string shortcut alongside the full update object. Reduces boilerplate for the most common case.

Bookkeeping

  • _bmad-output/implementation-artifacts/sprint-status.yaml: epic-1 and stories 1-1..1-4 flipped to done
  • _bmad-output/implementation-artifacts/1-1..1-4*.md: story records committed for traceability

Test Plan

  • Run mise run package and confirm skill/assets/source/ regenerates cleanly
  • Run mise run ci end to end (test + asset refresh)
  • Run skill/scripts/install.sh against a fresh target directory and verify the skill package installs and SKILL.md is discoverable
  • Smoke test the new updateChecklistItem(cardId, itemId, "complete") string shortcut and confirm it produces the same payload as the object form
  • Verify README install/usage instructions are accurate against the bundled artifacts

Out of Scope

  • Future epics in the skill-package sprint
  • Wider refactor of the trello-client API surface (the ergonomics change here is intentionally narrow)

🤖 Generated with Claude Code

…mics

Bundles the Trello MCP skill so it can be installed and consumed as a
self-contained package. Closes Epic 1 (stories 1-1 through 1-4) of the
skill-package sprint.

Epic 1 contents:

- 1-1 Skill directory structure (skill/assets/, skill/references/)
- 1-2 SKILL.md authored as the skill workflow entry point
- 1-3 install.sh hardened for bundle installation; build-skill-assets.sh
       added under scripts/ to refresh the bundled source snapshot
- 1-4 README updated to document skill installation and usage

Tooling:

- mise.toml: adds `package` task (refresh skill assets) and `ci` task
  (npm test + asset refresh)
- .gitignore: excludes local Codex state and Claude Flow data caches

Client ergonomics (small, ships with this PR):

- src/trello-client.ts: updateChecklistItem now accepts a string state
  shortcut (e.g. "complete") in addition to the full update object,
  reducing call-site boilerplate for the most common case

Sprint status flipped to done across epic-1 and all four stories;
implementation artifacts (1-1..1-4 story records) committed for
traceability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 7, 2026 16:07

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the Trello MCP server into a self-contained, BMAD-compatible skill package, adding necessary documentation, asset-bundling scripts, and health monitoring endpoints. It also updates the updateChecklistItem method to support state updates. The review identified several actionable issues: an incorrect rate-limiter calculation, a proxy configuration bypass in attachment downloads, redundant rate-limiter calls, a version mismatch between the server and package.json, unused dead code in validators.ts, and an unused property in health-monitor.ts.

Comment thread skill/assets/source/src/rate-limiter.ts
await this.rateLimiter.waitForAvailableToken();
await this.rateLimiter.waitForAvailableToken();
const response = await axios.get(downloadUrl, {
headers: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using the global axios instance here bypasses the proxy configuration established in the constructor (lines 72-78). This will cause attachment downloads to fail in environments requiring an HTTPS proxy. It is recommended to use this.axiosInstance (which handles absolute URLs correctly) or ensure the proxy agent is passed to this request.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@claude Make the change

Comment thread skill/assets/source/src/trello-client.ts Outdated
Comment thread skill/assets/source/src/index.ts Outdated

this.server = new McpServer({
name: 'trello-server',
version: '1.0.0',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The version string here ('1.0.0') does not match the version in package.json ('1.7.1'). It is better to keep these in sync, ideally by importing the version from package.json.

@delorenj delorenj May 7, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@claude Implement a 'version_bump' mise task that is invoked via git tag action

Comment thread skill/assets/source/src/validators.ts Outdated
Comment on lines +1 to +259
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';

export function validateString(value: unknown, field: string): string {
if (typeof value !== 'string') {
throw new McpError(ErrorCode.InvalidParams, `${field} must be a string`);
}
return value;
}

export function validateOptionalString(value: unknown): string | undefined {
if (value === undefined) return undefined;
return validateString(value, 'value');
}

export function validateNumber(value: unknown, field: string): number {
if (typeof value !== 'number') {
throw new McpError(ErrorCode.InvalidParams, `${field} must be a number`);
}
return value;
}

export function validateOptionalNumber(value: unknown): number | undefined {
if (value === undefined) return undefined;
return validateNumber(value, 'value');
}

export function validateStringArray(value: unknown): string[] {
if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) {
throw new McpError(ErrorCode.InvalidParams, 'Value must be an array of strings');
}
return value;
}

export function validateOptionalStringArray(value: unknown): string[] | undefined {
if (value === undefined) return undefined;
return validateStringArray(value);
}

export function validateBoolean(value: unknown, field: string): boolean {
if (typeof value !== 'boolean') {
throw new McpError(ErrorCode.InvalidParams, `${field} must be a boolean`);
}
return value;
}

export function validateOptionalBoolean(value: unknown): boolean | undefined {
if (value === undefined) return undefined;
return validateBoolean(value, 'value');
}

export function validateOptionalDateString(value: unknown): string | undefined {
if (value === undefined) return undefined;
const dateStr = validateString(value, 'date');
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
throw new McpError(ErrorCode.InvalidParams, 'date must be in YYYY-MM-DD format');
}
return dateStr;
}

export function validateGetCardsListRequest(args: Record<string, unknown>): {
boardId?: string;
listId: string;
} {
if (!args.listId) {
throw new McpError(ErrorCode.InvalidParams, 'listId is required');
}
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
listId: validateString(args.listId, 'listId'),
};
}

export function validateGetListsRequest(args: Record<string, unknown>): { boardId?: string } {
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
};
}

export function validateGetRecentActivityRequest(args: Record<string, unknown>): {
boardId?: string;
limit?: number;
} {
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
limit: validateOptionalNumber(args.limit),
};
}

export function validateAddCardRequest(args: Record<string, unknown>): {
boardId?: string;
listId: string;
name: string;
description?: string;
dueDate?: string;
start?: string;
labels?: string[];
} {
if (!args.listId || !args.name) {
throw new McpError(ErrorCode.InvalidParams, 'listId and name are required');
}

return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
listId: validateString(args.listId, 'listId'),
name: validateString(args.name, 'name'),
description: validateOptionalString(args.description),
dueDate: validateOptionalString(args.dueDate),
start: validateOptionalDateString(args.start),
labels: validateOptionalStringArray(args.labels),
};
}

export function validateUpdateCardRequest(args: Record<string, unknown>): {
boardId?: string;
cardId: string;
name?: string;
description?: string;
dueDate?: string;
start?: string;
dueComplete?: boolean;
labels?: string[];
} {
if (!args.cardId) {
throw new McpError(ErrorCode.InvalidParams, 'cardId is required');
}
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
cardId: validateString(args.cardId, 'cardId'),
name: validateOptionalString(args.name),
description: validateOptionalString(args.description),
dueDate: validateOptionalString(args.dueDate),
start: validateOptionalDateString(args.start),
dueComplete: validateOptionalBoolean(args.dueComplete),
labels: validateOptionalStringArray(args.labels),
};
}

export function validateArchiveCardRequest(args: Record<string, unknown>): {
boardId?: string;
cardId: string;
} {
if (!args.cardId) {
throw new McpError(ErrorCode.InvalidParams, 'cardId is required');
}
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
cardId: validateString(args.cardId, 'cardId'),
};
}

export function validateAddListRequest(args: Record<string, unknown>): {
boardId?: string;
name: string;
} {
if (!args.name) {
throw new McpError(ErrorCode.InvalidParams, 'name is required');
}
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
name: validateString(args.name, 'name'),
};
}

export function validateArchiveListRequest(args: Record<string, unknown>): {
boardId?: string;
listId: string;
} {
if (!args.listId) {
throw new McpError(ErrorCode.InvalidParams, 'listId is required');
}
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
listId: validateString(args.listId, 'listId'),
};
}

export function validateMoveCardRequest(args: Record<string, unknown>): {
boardId?: string;
cardId: string;
listId: string;
} {
if (!args.cardId || !args.listId) {
throw new McpError(ErrorCode.InvalidParams, 'cardId and listId are required');
}
return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
cardId: validateString(args.cardId, 'cardId'),
listId: validateString(args.listId, 'listId'),
};
}

export function validateAttachImageRequest(args: Record<string, unknown>): {
boardId?: string;
cardId: string;
imageUrl: string;
name?: string;
} {
if (!args.cardId || !args.imageUrl) {
throw new McpError(ErrorCode.InvalidParams, 'cardId and imageUrl are required');
}

const imageUrl = validateString(args.imageUrl, 'imageUrl');
try {
new URL(imageUrl);
} catch (e) {
throw new McpError(ErrorCode.InvalidParams, 'imageUrl must be a valid URL');
}

return {
boardId: args.boardId ? validateString(args.boardId, 'boardId') : undefined,
cardId: validateString(args.cardId, 'cardId'),
imageUrl: imageUrl,
name: validateOptionalString(args.name),
};
}

export function validateSetActiveBoardRequest(args: Record<string, unknown>): { boardId: string } {
if (!args.boardId) {
throw new McpError(ErrorCode.InvalidParams, 'boardId is required');
}
return {
boardId: validateString(args.boardId, 'boardId'),
};
}

export function validateSetActiveWorkspaceRequest(args: Record<string, unknown>): {
workspaceId: string;
} {
if (!args.workspaceId) {
throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required');
}
return {
workspaceId: validateString(args.workspaceId, 'workspaceId'),
};
}

export function validateListBoardsInWorkspaceRequest(args: Record<string, unknown>): {
workspaceId: string;
} {
if (!args.workspaceId) {
throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required');
}
return {
workspaceId: validateString(args.workspaceId, 'workspaceId'),
};
}

export function validateGetCardRequest(args: Record<string, unknown>): {
cardId: string;
includeMarkdown?: boolean;
} {
if (!args.cardId) {
throw new McpError(ErrorCode.InvalidParams, 'cardId is required');
}
return {
cardId: validateString(args.cardId, 'cardId'),
includeMarkdown: validateOptionalBoolean(args.includeMarkdown),
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This file appears to be unused dead code. The server implementation in index.ts uses zod for request validation, and no other files in the source bundle import these manual validator functions. Removing it would improve project cleanliness.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@copilot delete it

private performanceTracker: PerformanceTracker;
private lastHealthCheck?: SystemHealthReport;
private readonly trelloClient: TrelloClient;
private rateLimiter: any; // Will get injected from TrelloClient

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The rateLimiter property is unused and the comment about injection is not reflected in the current implementation. This property should be removed to avoid confusion.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d6a31241b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread skill/scripts/install.sh Outdated
mkdir -p "$INSTALL_DIR/build"
cat >"$BUILD_FILE" <<'EOF'
#!/usr/bin/env node
import { spawn } from 'node:child_process';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit CommonJS fallback wrapper for broader Node compatibility

When Bun is unavailable, the installer writes build/index.js using ESM syntax (import { spawn } ...) but does not create a nearby package.json with "type": "module" (or use .mjs). In environments where node still interprets .js as CommonJS (notably older Node LTS setups), running node .../build/index.js fails before startup with an import-syntax error, so the fallback install path cannot launch the MCP server at all.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@claude

2026-05-07-14:58 - Node Fallback Compatibility Fix

Objective

Ensure the Node.js fallback path for the MCP server successfully launches in environments without Bun by resolving ESM/CommonJS syntax friction.

Strategy

Update the installer to [insert choice: emit .mjs extension OR emit CJS require syntax] to ensure native Node LTS compatibility without requiring a localized package.json.

Acceptance Criteria

  1. Executing the generated file in a Node LTS environment (v18/v20) successfully initializes the MCP server.
  2. The process does not throw SyntaxError: Cannot use import statement outside a module.
  3. The primary Bun execution path remains unaffected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 completes “Epic 1” of the skill-package sprint by packaging Trello MCP as a self-contained BMAD-compatible skill bundle (docs, references, bundled source, and install workflow), and includes a small Trello client ergonomics tweak.

Changes:

  • Add skill/ bundle structure (SKILL entrypoint, references, bundled server source) and a hardened first-run installer.
  • Add packaging/CI tasks (mise run package, mise run ci) and an asset refresh script to rebuild skill/assets/source/.
  • Extend updateChecklistItem to accept a bare state string ('complete' | 'incomplete') in addition to an update object.

Reviewed changes

Copilot reviewed 28 out of 30 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/trello-client.ts Accept checklist item state strings by normalizing to an update object.
skill/SKILL.md Defines skill activation flow, routing, tool grouping, and agent rules.
skill/scripts/install.sh Installs/builds bundled server into user data dir, with registry fallback.
skill/references/trello-mcp/README.md Entry-point router for loading the right reference doc.
skill/references/trello-mcp/configuration.md Install + MCP client configuration guidance for the skill bundle.
skill/references/trello-mcp/api.md Tool-name reference grouped by capability.
skill/references/trello-mcp/patterns.md Suggested operational workflows for common Trello tasks.
skill/references/trello-mcp/gotchas.md Constraints and pitfalls (IDs, dates, rate limits, destructive actions).
skill/assets/source/tsconfig.json Bundled server TypeScript compiler configuration.
skill/assets/source/src/validators.ts Bundled server input validation helpers.
skill/assets/source/src/types.ts Bundled server type definitions (Trello + MCP-facing types).
skill/assets/source/src/trello-client.ts Bundled server Trello client implementation (snapshot).
skill/assets/source/src/rate-limiter.ts Bundled server rate limiter implementation (snapshot).
skill/assets/source/src/index.ts Bundled server MCP tool registration and stdio server entrypoint (snapshot).
skill/assets/source/src/health/health-monitor.ts Bundled health monitor logic (snapshot).
skill/assets/source/src/health/health-endpoints.ts Bundled MCP health tools/endpoints (snapshot).
skill/assets/source/src/evals/evals.ts Bundled eval configuration (snapshot).
skill/assets/source/smithery.yaml Bundled Smithery manifest for registry-based distribution path.
skill/assets/source/package.json Bundled server package manifest used for local install/build.
skill/assets/source/LICENSE Bundled license file included in source snapshot.
scripts/build-skill-assets.sh Script to refresh skill/assets/source/ from repo source.
README.md Documents skill installation/usage and packaging workflow for maintainers.
mise.toml Adds package and ci tasks (refresh assets; tests + refresh).
.gitignore Ignores local Codex/Claude Flow caches.
_bmad-output/implementation-artifacts/sprint-status.yaml Marks epic/stories as done.
_bmad-output/implementation-artifacts/1-1-initialize-skill-directory-structure.md Story record for traceability.
_bmad-output/implementation-artifacts/1-2-create-skill-md-workflow.md Story record for traceability.
_bmad-output/implementation-artifacts/1-3-bundle-install-script.md Story record for traceability.
_bmad-output/implementation-artifacts/1-4-update-documentation.md Story record for traceability.

Comment thread skill/scripts/install.sh
Comment on lines +31 to +53
cat >"$BUILD_FILE" <<'EOF'
#!/usr/bin/env node
import { spawn } from 'node:child_process';

const child = spawn('npx', ['-y', '@delorenj/mcp-server-trello', ...process.argv.slice(2)], {
stdio: 'inherit',
env: process.env,
});

child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}

process.exit(code ?? 1);
});

child.on('error', (error) => {
console.error(`Failed to start @delorenj/mcp-server-trello via npx: ${error.message}`);
process.exit(1);
});
EOF

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feedback is already applied in commit 4e939e2: the fallback wrapper generated by skill/scripts/install.sh now uses CommonJS (require) so node build/index.js works in default Node LTS CommonJS environments. No UI changes were involved.

Comment thread skill/assets/source/src/rate-limiter.ts Outdated
} else {
// Calculate time until next token is available
const tokensNeeded = 1 - this.tokens;
const msToWait = (tokensNeeded / this.refillRate) * 1000;
delorenj and others added 2 commits May 7, 2026 14:53
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@delorenj

delorenj commented May 7, 2026

Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

Copilot AI commented May 7, 2026

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Applied the review-thread fixes in commit 4e939e2:

  • switched the generated fallback wrapper in skill/scripts/install.sh to CommonJS so node build/index.js runs correctly
  • corrected rate limiter wait-time units in both src/rate-limiter.ts and skill/assets/source/src/rate-limiter.ts by removing the extra * 1000

No UI changes were involved.

- Route downloadAttachment through axiosInstance to honor HTTPS_PROXY
  configuration and the rate-limiting interceptor (drops the redundant
  duplicate waitForAvailableToken call that bypassed the proxy via raw
  axios.get).
- Sync server version string to 1.7.1 to match package.json.
- Remove unused validators.ts module (no callers in current src/index.ts;
  superseded by inline zod schemas) and its now-orphaned unit test.
- Refresh skill/assets/source bundle.
@delorenj
delorenj merged commit 50f56da into main May 9, 2026
2 checks passed
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.

3 participants