Skip to content

Commit 9eb21f0

Browse files
authored
feat: modernize dependencies and ensure Zod v3.25.76 MCP SDK compatibility (#80)
* feat: modernize dependencies and migrate to Zod v4 - Update all dependencies to latest versions - Migrate from Zod v3 to v4.1.5 - Update error handling from .errors to .issues for Zod v4 compatibility - Fix handler signatures for MCP SDK compatibility - Rename schema fields to avoid MCP SDK conflicts (title -> pageTitle, searchTitle) - Update z.record() calls to include both key and value types - Ensure all tests pass with updated dependencies * feat: enhance Jest configuration for cleaner test output - Add test:quiet script for minimal console output during testing - Configure Jest with explicit silent/verbose settings - Add setupFilesAfterEnv array for future test setup flexibility - Optimize development workflow with cleaner test feedback All tests pass (170/170) with zero warnings or errors. * fix(deps): downgrade Zod to v3.25.76 for MCP SDK compatibility in Confluence server - Resolve MCP server compatibility with Zod v4 - Successfully tested HTTP streamable transport on port 3033 - All MCP tools verified working with live Confluence API integration - Comprehensive curl testing completed for read-only operations - All quality checks passed (lint, format, build, test) * fix: update package-lock.json with zod v3.25.76 compatibility - Reverted from zod v4.1.5 to v3.25.76 for MCP SDK compatibility - Fixed keyValidator._parse errors in MCP tool registrations - Verified all tools working with proper .shape references - Completed clean reinstall with full test verification
1 parent c894f6c commit 9eb21f0

File tree

8 files changed

+541
-488
lines changed

8 files changed

+541
-488
lines changed

package-lock.json

Lines changed: 446 additions & 443 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"prepare": "npm run build && node scripts/ensure-executable.js",
1818
"postinstall": "node scripts/ensure-executable.js",
1919
"test": "jest",
20+
"test:quiet": "jest --silent",
2021
"test:coverage": "jest --coverage",
2122
"test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
2223
"lint": "eslint src --ext .ts --config eslint.config.mjs",
@@ -61,42 +62,42 @@
6162
"author": "",
6263
"license": "ISC",
6364
"devDependencies": {
64-
"@eslint/js": "^9.33.0",
65+
"@eslint/js": "^9.35.0",
6566
"@semantic-release/changelog": "^6.0.3",
6667
"@semantic-release/exec": "^7.1.0",
6768
"@semantic-release/git": "^10.0.1",
68-
"@semantic-release/github": "^11.0.4",
69+
"@semantic-release/github": "^11.0.5",
6970
"@semantic-release/npm": "^12.0.2",
7071
"@types/cors": "^2.8.19",
7172
"@types/express": "^5.0.3",
7273
"@types/jest": "^30.0.0",
73-
"@types/node": "^24.3.0",
74+
"@types/node": "^24.3.1",
7475
"@types/turndown": "^5.0.5",
75-
"@typescript-eslint/eslint-plugin": "^8.39.1",
76-
"@typescript-eslint/parser": "^8.39.1",
77-
"eslint": "^9.33.0",
76+
"@typescript-eslint/eslint-plugin": "^8.43.0",
77+
"@typescript-eslint/parser": "^8.43.0",
78+
"eslint": "^9.35.0",
7879
"eslint-config-prettier": "^10.1.8",
7980
"eslint-plugin-filenames": "^1.3.2",
8081
"eslint-plugin-prettier": "^5.5.4",
81-
"jest": "^30.0.5",
82+
"jest": "^30.1.3",
8283
"nodemon": "^3.1.10",
83-
"npm-check-updates": "^18.0.2",
84+
"npm-check-updates": "^18.1.0",
8485
"prettier": "^3.6.2",
8586
"semantic-release": "^24.2.7",
8687
"ts-jest": "^29.4.1",
8788
"ts-node": "^10.9.2",
8889
"typescript": "^5.9.2",
89-
"typescript-eslint": "^8.39.1"
90+
"typescript-eslint": "^8.43.0"
9091
},
9192
"publishConfig": {
9293
"registry": "https://registry.npmjs.org/",
9394
"access": "public"
9495
},
9596
"dependencies": {
96-
"@modelcontextprotocol/sdk": "^1.17.3",
97+
"@modelcontextprotocol/sdk": "^1.17.5",
9798
"commander": "^14.0.0",
9899
"cors": "^2.8.5",
99-
"dotenv": "^17.2.1",
100+
"dotenv": "^17.2.2",
100101
"express": "^5.1.0",
101102
"turndown": "^7.2.1",
102103
"zod": "^3.25.76"
@@ -146,7 +147,10 @@
146147
"jsx",
147148
"json",
148149
"node"
149-
]
150+
],
151+
"silent": false,
152+
"verbose": false,
153+
"setupFilesAfterEnv": []
150154
},
151155
"engines": {
152156
"node": ">=18.0.0"

src/services/vendor.atlassian.pages.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const WebResourceSchema = z.object({
138138
key: z.string(),
139139
contexts: z.array(z.string()),
140140
superbatch: z.string(),
141-
uris: z.record(z.string()),
141+
uris: z.record(z.string(), z.string()),
142142
tags: z.array(z.string()),
143143
});
144144

src/tools/atlassian.comments.tool.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,18 @@ type ListPageCommentsArgs = z.infer<typeof ListPageCommentsArgsSchema>;
5454
* Handle the request to list comments for a page
5555
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted comments list
5656
*/
57-
async function handleListPageComments(args: ListPageCommentsArgs) {
57+
async function handleListPageComments(args: Record<string, unknown>) {
5858
const methodLogger = logger.forMethod('handleListPageComments');
5959

6060
try {
6161
methodLogger.debug('Tool conf_ls_page_comments called', args);
6262

6363
// Call the controller with original args
64+
const typedArgs = args as ListPageCommentsArgs;
6465
const result = await atlassianCommentsController.listPageComments({
65-
pageId: args.pageId,
66-
limit: args.limit,
67-
start: args.start,
66+
pageId: typedArgs.pageId,
67+
limit: typedArgs.limit,
68+
start: typedArgs.start,
6869
});
6970

7071
// Format the response for MCP

src/tools/atlassian.inline-comments.tool.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,20 @@ type ListInlineCommentsArgs = z.infer<typeof ListInlineCommentsArgsSchema>;
7676
* Handle the request to list inline comments only for a page
7777
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted inline comments list
7878
*/
79-
async function handleListInlineComments(args: ListInlineCommentsArgs) {
79+
async function handleListInlineComments(args: Record<string, unknown>) {
8080
const methodLogger = logger.forMethod('handleListInlineComments');
8181

8282
try {
8383
methodLogger.debug('Tool conf_ls_inline_comments called', args);
8484

8585
// Call the new controller method for inline comments
86+
const typedArgs = args as ListInlineCommentsArgs;
8687
const result = await atlassianCommentsController.listInlineComments({
87-
pageId: args.pageId,
88-
includeResolved: args.includeResolved,
89-
sortBy: args.sortBy,
90-
limit: args.limit,
91-
start: args.start,
88+
pageId: typedArgs.pageId,
89+
includeResolved: typedArgs.includeResolved,
90+
sortBy: typedArgs.sortBy,
91+
limit: typedArgs.limit,
92+
start: typedArgs.start,
9293
});
9394

9495
// Format the response for MCP

src/tools/atlassian.pages.tool.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { z } from 'zod';
23
import { Logger } from '../utils/logger.util.js';
34
import { formatErrorForMcpTool } from '../utils/error.util.js';
45
import atlassianPagesController from '../controllers/atlassian.pages.controller.js';
56
import {
6-
ListPagesToolArgsType,
7+
type ListPagesToolArgsType,
78
ListPagesToolArgs,
8-
GetPageToolArgsType,
9+
type GetPageToolArgsType,
910
GetPageToolArgs,
1011
} from './atlassian.pages.types.js';
1112

@@ -19,7 +20,7 @@ import {
1920
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted pages list
2021
* @throws Will return error message if page listing fails
2122
*/
22-
async function listPages(args: ListPagesToolArgsType) {
23+
async function listPages(args: Record<string, unknown>) {
2324
const methodLogger = Logger.forContext(
2425
'tools/atlassian.pages.tool.ts',
2526
'listPages',
@@ -30,7 +31,9 @@ async function listPages(args: ListPagesToolArgsType) {
3031
methodLogger.debug('Calling controller with options:', args);
3132

3233
// With updated controller signature, we can pass the tool args directly
33-
const result = await atlassianPagesController.list(args);
34+
const result = await atlassianPagesController.list(
35+
args as ListPagesToolArgsType,
36+
);
3437

3538
methodLogger.debug('Successfully retrieved pages list');
3639

@@ -58,7 +61,7 @@ async function listPages(args: ListPagesToolArgsType) {
5861
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted page details
5962
* @throws Will return error message if page retrieval fails
6063
*/
61-
async function getPage(args: GetPageToolArgsType) {
64+
async function getPage(args: Record<string, unknown>) {
6265
const methodLogger = Logger.forContext(
6366
'tools/atlassian.pages.tool.ts',
6467
'getPage',
@@ -67,7 +70,9 @@ async function getPage(args: GetPageToolArgsType) {
6770

6871
try {
6972
// Call the controller to get page details - we can now pass args directly
70-
const result = await atlassianPagesController.get(args);
73+
const result = await atlassianPagesController.get(
74+
args as GetPageToolArgsType,
75+
);
7176

7277
methodLogger.debug('Successfully retrieved page details');
7378

@@ -103,16 +108,32 @@ function registerTools(server: McpServer) {
103108
toolLogger.debug('Registering Atlassian Pages tools...');
104109

105110
// Register the list pages tool
111+
// Rename title field to avoid MCP SDK conflict
112+
const listPagesSchema = z.object({
113+
spaceIds: ListPagesToolArgs.shape.spaceIds,
114+
spaceKeys: ListPagesToolArgs.shape.spaceKeys,
115+
parentId: ListPagesToolArgs.shape.parentId,
116+
pageTitle: ListPagesToolArgs.shape.title, // Renamed from 'title' to 'pageTitle'
117+
status: ListPagesToolArgs.shape.status,
118+
limit: ListPagesToolArgs.shape.limit,
119+
cursor: ListPagesToolArgs.shape.cursor,
120+
sort: ListPagesToolArgs.shape.sort,
121+
});
106122
server.tool(
107123
'conf_ls_pages',
108-
`Lists pages within specified spaces (by \`spaceId\` or \`spaceKey\`) or globally. Filters by \`title\` with SMART MATCHING: tries exact match first, automatically falls back to partial matching if no exact results found. Supports \`status\` (current, archived, etc.), sorting (\`sort\`) and pagination (\`limit\`, \`cursor\`).
124+
`Lists pages within specified spaces (by \`spaceId\` or \`spaceKey\`) or globally. Filters by \`pageTitle\` with SMART MATCHING: tries exact match first, automatically falls back to partial matching if no exact results found. Supports \`status\` (current, archived, etc.), sorting (\`sort\`) and pagination (\`limit\`, \`cursor\`).
109125
- Returns a formatted list of pages including ID, title, status, space ID, author, version, and URL.
110126
- Pagination information including next cursor value is included at the end of the returned text content.
111-
- SMART TITLE SEARCH: When using \`title\` parameter, if exact match fails, automatically searches for partial matches (e.g., "Balance" will find "Balance Reconciliation System").
127+
- SMART TITLE SEARCH: When using \`pageTitle\` parameter, if exact match fails, automatically searches for partial matches (e.g., "Balance" will find "Balance Reconciliation System").
112128
- For full-text content search or advanced queries, use the \`conf_search\` tool.
113129
- Requires Confluence credentials.`,
114-
ListPagesToolArgs.shape,
115-
listPages,
130+
listPagesSchema.shape,
131+
async (args: Record<string, unknown>) => {
132+
// Map pageTitle back to title for the controller
133+
const mappedArgs = { ...args, title: args.pageTitle };
134+
delete (mappedArgs as Record<string, unknown>).pageTitle;
135+
return listPages(mappedArgs);
136+
},
116137
);
117138

118139
// Register the get page details tool

src/tools/atlassian.search.tool.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { z } from 'zod';
23
import { Logger } from '../utils/logger.util.js';
34
import { formatErrorForMcpTool } from '../utils/error.util.js';
45
import atlassianSearchController from '../controllers/atlassian.search.controller.js';
56
import {
6-
SearchToolArgsType,
7+
type SearchToolArgsType,
78
SearchToolArgs,
89
} from './atlassian.search.types.js';
910

@@ -17,7 +18,7 @@ import {
1718
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted search results
1819
* @throws Will return error message if search fails
1920
*/
20-
async function searchContent(args: SearchToolArgsType) {
21+
async function searchContent(args: Record<string, unknown>) {
2122
const methodLogger = Logger.forContext(
2223
'tools/atlassian.search.tool.ts',
2324
'searchContent',
@@ -26,7 +27,9 @@ async function searchContent(args: SearchToolArgsType) {
2627

2728
try {
2829
// Call the controller to search content
29-
const result = await atlassianSearchController.search(args);
30+
const result = await atlassianSearchController.search(
31+
args as SearchToolArgsType,
32+
);
3033

3134
methodLogger.debug('Successfully searched Confluence content');
3235

@@ -63,17 +66,33 @@ function registerTools(server: McpServer) {
6366
toolLogger.debug('Registering Atlassian Search tools...');
6467

6568
// Register the search content tool
69+
// Rename title field to avoid MCP SDK conflict
70+
const searchSchema = z.object({
71+
limit: SearchToolArgs.shape.limit,
72+
cursor: SearchToolArgs.shape.cursor,
73+
cql: SearchToolArgs.shape.cql,
74+
searchTitle: SearchToolArgs.shape.title, // Renamed from 'title' to 'searchTitle'
75+
spaceKey: SearchToolArgs.shape.spaceKey,
76+
labels: SearchToolArgs.shape.labels,
77+
contentType: SearchToolArgs.shape.contentType,
78+
query: SearchToolArgs.shape.query,
79+
});
6680
server.tool(
6781
'conf_search',
68-
`Searches Confluence content. Supports multiple filter options: \`cql\` (for providing a complete custom Confluence Query Language string), \`title\` (text in title), \`spaceKey\`, \`labels\`, and \`contentType\` (page/blogpost). A general \`query\` parameter performs a basic text search (equivalent to CQL: text ~ "your query").
82+
`Searches Confluence content. Supports multiple filter options: \`cql\` (for providing a complete custom Confluence Query Language string), \`searchTitle\` (text in title), \`spaceKey\`, \`labels\`, and \`contentType\` (page/blogpost). A general \`query\` parameter performs a basic text search (equivalent to CQL: text ~ "your query").
6983
- IMPORTANT for \`cql\` users: Ensure your CQL syntax is correct, especially quoting terms in text searches (e.g., \`text ~ "search phrase"\`). Invalid CQL will result in an error. Refer to official Confluence CQL documentation.
7084
- Filters are generally combined with AND logic.
7185
- Supports pagination (\`limit\`, \`cursor\`).
7286
- The executed CQL and pagination information (including next cursor value) are included directly in the returned text content.
7387
- Returns Markdown formatted results with snippets and metadata.
7488
- Requires Confluence credentials.`,
75-
SearchToolArgs.shape,
76-
searchContent,
89+
searchSchema.shape,
90+
async (args: Record<string, unknown>) => {
91+
// Map searchTitle back to title for the controller
92+
const mappedArgs = { ...args, title: args.searchTitle };
93+
delete (mappedArgs as Record<string, unknown>).searchTitle;
94+
return searchContent(mappedArgs);
95+
},
7796
);
7897

7998
toolLogger.debug('Successfully registered Atlassian Search tools');

src/tools/atlassian.spaces.tool.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Logger } from '../utils/logger.util.js';
33
import { formatErrorForMcpTool } from '../utils/error.util.js';
44
import {
55
ListSpacesToolArgs,
6-
ListSpacesToolArgsType,
7-
GetSpaceToolArgsType,
6+
type ListSpacesToolArgsType,
7+
type GetSpaceToolArgsType,
88
GetSpaceToolArgs,
99
} from './atlassian.spaces.types.js';
1010

@@ -20,7 +20,7 @@ import atlassianSpacesController from '../controllers/atlassian.spaces.controlle
2020
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted spaces list
2121
* @throws Will return error message if space listing fails
2222
*/
23-
async function listSpaces(args: ListSpacesToolArgsType) {
23+
async function listSpaces(args: Record<string, unknown>) {
2424
const toolLogger = Logger.forContext(
2525
'tools/atlassian.spaces.tool.ts',
2626
'listSpaces',
@@ -29,7 +29,9 @@ async function listSpaces(args: ListSpacesToolArgsType) {
2929

3030
try {
3131
// Pass the args directly to the controller without any transformation
32-
const result = await atlassianSpacesController.list(args);
32+
const result = await atlassianSpacesController.list(
33+
args as ListSpacesToolArgsType,
34+
);
3335

3436
toolLogger.debug('Successfully retrieved spaces from controller');
3537

@@ -57,7 +59,7 @@ async function listSpaces(args: ListSpacesToolArgsType) {
5759
* @returns {Promise<{ content: Array<{ type: 'text', text: string }> }>} MCP response with formatted space details
5860
* @throws Will return error message if space retrieval fails
5961
*/
60-
async function getSpace(args: GetSpaceToolArgsType) {
62+
async function getSpace(args: Record<string, unknown>) {
6163
const methodLogger = Logger.forContext(
6264
'tools/atlassian.spaces.tool.ts',
6365
'getSpace',
@@ -66,7 +68,9 @@ async function getSpace(args: GetSpaceToolArgsType) {
6668

6769
try {
6870
// Call the controller to get space details with args directly
69-
const result = await atlassianSpacesController.get(args);
71+
const result = await atlassianSpacesController.get(
72+
args as GetSpaceToolArgsType,
73+
);
7074

7175
methodLogger.debug('Successfully retrieved space details');
7276

0 commit comments

Comments
 (0)