This is page 1 of 3. Use http://codebase.md/phuc-nt/mcp-atlassian-server?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .gitignore
├── .npmignore
├── assets
│ ├── atlassian_logo_icon.png
│ └── atlassian_logo_icon.webp
├── CHANGELOG.md
├── dev_mcp-atlassian-test-client
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── list-mcp-inventory.ts
│ │ ├── test-confluence-pages.ts
│ │ ├── test-confluence-spaces.ts
│ │ ├── test-jira-issues.ts
│ │ ├── test-jira-projects.ts
│ │ ├── test-jira-users.ts
│ │ └── tool-test.ts
│ └── tsconfig.json
├── docker-compose.yml
├── Dockerfile
├── docs
│ ├── dev-guide
│ │ ├── advance-resource-tool-2.md
│ │ ├── advance-resource-tool-3.md
│ │ ├── advance-resource-tool.md
│ │ ├── confluence-migrate-to-v2.md
│ │ ├── github-community-exchange.md
│ │ ├── marketplace-publish-application-template.md
│ │ ├── marketplace-publish-guideline.md
│ │ ├── mcp-client-for-testing.md
│ │ ├── mcp-overview.md
│ │ ├── migrate-api-v2-to-v3.md
│ │ ├── mini-plan-refactor-tools.md
│ │ ├── modelcontextprotocol-architecture.md
│ │ ├── modelcontextprotocol-introduction.md
│ │ ├── modelcontextprotocol-resources.md
│ │ ├── modelcontextprotocol-tools.md
│ │ ├── one-click-setup.md
│ │ ├── prompts.md
│ │ ├── release-with-prebuild-bundle.md
│ │ ├── resource-metadata-schema-guideline.md
│ │ ├── resources.md
│ │ ├── sampling.md
│ │ ├── schema-metadata.md
│ │ ├── stdio-transport.md
│ │ ├── tool-vs-resource.md
│ │ ├── tools.md
│ │ └── workflow-examples.md
│ ├── introduction
│ │ ├── marketplace-submission.md
│ │ └── resources-and-tools.md
│ ├── knowledge
│ │ ├── 01-mcp-overview-architecture.md
│ │ ├── 02-mcp-tools-resources.md
│ │ ├── 03-mcp-prompts-sampling.md
│ │ ├── building-mcp-server.md
│ │ └── client-development-guide.md
│ ├── plan
│ │ ├── history.md
│ │ ├── roadmap.md
│ │ └── todo.md
│ └── test-reports
│ ├── cline-installation-test-2025-05-04.md
│ └── cline-test-2025-04-20.md
├── jest.config.js
├── LICENSE
├── llms-install-bundle.md
├── llms-install.md
├── package-lock.json
├── package.json
├── README.md
├── RELEASE_NOTES.md
├── smithery.yaml
├── src
│ ├── index.ts
│ ├── resources
│ │ ├── confluence
│ │ │ ├── index.ts
│ │ │ ├── pages.ts
│ │ │ └── spaces.ts
│ │ ├── index.ts
│ │ └── jira
│ │ ├── boards.ts
│ │ ├── dashboards.ts
│ │ ├── filters.ts
│ │ ├── index.ts
│ │ ├── issues.ts
│ │ ├── projects.ts
│ │ ├── sprints.ts
│ │ └── users.ts
│ ├── schemas
│ │ ├── common.ts
│ │ ├── confluence.ts
│ │ └── jira.ts
│ ├── tests
│ │ ├── confluence
│ │ │ └── create-page.test.ts
│ │ └── e2e
│ │ └── mcp-server.test.ts
│ ├── tools
│ │ ├── confluence
│ │ │ ├── add-comment.ts
│ │ │ ├── create-page.ts
│ │ │ ├── delete-footer-comment.ts
│ │ │ ├── delete-page.ts
│ │ │ ├── update-footer-comment.ts
│ │ │ ├── update-page-title.ts
│ │ │ └── update-page.ts
│ │ ├── index.ts
│ │ └── jira
│ │ ├── add-gadget-to-dashboard.ts
│ │ ├── add-issue-to-sprint.ts
│ │ ├── add-issues-to-backlog.ts
│ │ ├── assign-issue.ts
│ │ ├── close-sprint.ts
│ │ ├── create-dashboard.ts
│ │ ├── create-filter.ts
│ │ ├── create-issue.ts
│ │ ├── create-sprint.ts
│ │ ├── delete-filter.ts
│ │ ├── get-gadgets.ts
│ │ ├── rank-backlog-issues.ts
│ │ ├── remove-gadget-from-dashboard.ts
│ │ ├── start-sprint.ts
│ │ ├── transition-issue.ts
│ │ ├── update-dashboard.ts
│ │ ├── update-filter.ts
│ │ └── update-issue.ts
│ └── utils
│ ├── atlassian-api-base.ts
│ ├── confluence-interfaces.ts
│ ├── confluence-resource-api.ts
│ ├── confluence-tool-api.ts
│ ├── error-handler.ts
│ ├── jira-interfaces.ts
│ ├── jira-resource-api.ts
│ ├── jira-tool-api-agile.ts
│ ├── jira-tool-api-v3.ts
│ ├── jira-tool-api.ts
│ ├── logger.ts
│ ├── mcp-core.ts
│ └── mcp-helpers.ts
├── start-docker.sh
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
# Atlassian Configuration
ATLASSIAN_SITE_NAME=your-site.atlassian.net
[email protected]
ATLASSIAN_API_TOKEN=your-api-token
# MCP Configuration
MCP_SERVER_NAME=mcp-atlassian-integration
MCP_SERVER_VERSION=1.0.0
# Logging Configuration
LOG_LEVEL=info
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
# Development directories
node_modules/
src/
tests/
dev_mcp-atlassian-test-client/
# Configuration files
.env
.env.example
.gitignore
tsconfig.json
jest.config.js
# Docker files
Dockerfile
docker-compose.yml
start-docker.sh
# Development files
*.log
*.tsbuildinfo
.vscode/
.idea/
# Test files
coverage/
*.test.ts
*.spec.ts
# Documentation (keep only essential docs)
docs/dev-guide/
docs/test-reports/
docs/plan/
# Other files
.git/
.github/
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
phuc-nt-mcp-atlassian-server-2.0.0.tgz
phuc-nt-mcp-atlassian-server-*.tgz
# Node modules
node_modules/
# Build files
dist/
# Coverage reports
coverage/
# Environment variables
.env
.env.local
.env.development
.env.test
.env.production
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea/
.vscode/
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Temporary files
.tmp/
.temp/
mcp-atlassian-server-bundle*.zip
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP Atlassian Server (by phuc-nt)
<p align="center">
<img src="assets/atlassian_logo_icon.png" alt="Atlassian Logo" width="120" />
</p>
[](https://github.com/phuc-nt/mcp-atlassian-server)
[](https://smithery.ai/server/@phuc-nt/mcp-atlassian-server)
## What's New in Version 2.1.1 🚀
- Refactored the entire codebase to standardize resource/tool structure, completely removed the content-metadata resource, and merged metadata into the page resource.
- New developer guide: anyone can now easily extend and maintain the codebase.
- Ensured compatibility with the latest MCP SDK, improved security, scalability, and maintainability.
- Updated `docs/introduction/resources-and-tools.md` to remove all references to content-metadata.
👉 **See the full [CHANGELOG](./CHANGELOG.md) for details.**
## What's New in Version 2.0.1 🎉
**MCP Atlassian Server v2.0.1** brings a major expansion of features and capabilities!
- **Updated APIs**: Now using the latest Atlassian APIs (Jira API v3, Confluence API v2)
- **Expanded Features**: Grown from 21 to 48 features, including advanced Jira and Confluence capabilities
- **Enhanced Board & Sprint Management**: Complete Agile/Scrum workflow support
- **Advanced Confluence Features**: Page version management, attachments handling, and comment management
- **Improved Resource Registration**: Fixed duplicate resource registration issues for a more stable experience
- **Documentation Update**: New comprehensive documentation series explaining MCP architecture, resource/tool development
For full details on all changes, improvements, and fixes, see the [CHANGELOG](./CHANGELOG.md).
## Introduction
**MCP Atlassian Server (by phuc-nt)** is a Model Context Protocol (MCP) server that connects AI agents like Cline, Claude Desktop, or Cursor to Atlassian Jira and Confluence, enabling them to query data and perform actions through a standardized interface.
> **Note:** This server is primarily designed and optimized for use with Cline, though it follows the MCP standard and can work with other MCP-compatible clients.

- **Key Features:**
- Connect AI agents to Atlassian Jira and Confluence
- Support both Resources (read-only) and Tools (actions/mutations)
- Easy integration with Cline through MCP Marketplace
- Local-first design for personal development environments
- Optimized integration with Cline AI assistant
## The Why Behind This Project
As a developer working daily with Jira and Confluence, I found myself spending significant time navigating these tools. While they're powerful, I longed for a simpler way to interact with them without constantly context-switching during deep work.
The emergence of AI Agents and the Model Context Protocol (MCP) presented the perfect opportunity. I immediately saw the potential to connect Jira and Confluence (with plans for Slack, GitHub, Calendar, and more) to my AI workflows.
This project began as a learning journey into MCP and AI Agents, but I hope it evolves into something truly useful for individuals and organizations who interact with Atlassian tools daily.
## System Architecture
```mermaid
graph TD
AI[Cline AI Assistant] <--> MCP[MCP Atlassian Server]
MCP <--> JiraAPI[Jira API]
MCP <--> ConfAPI[Confluence API]
subgraph "MCP Server"
Resources[Resources - Read Only]
Tools[Tools - Actions]
end
Resources --> JiraRes[Jira Resources<br/>issues, projects, users]
Resources --> ConfRes[Confluence Resources<br/>spaces, pages]
Tools --> JiraTools[Jira Tools<br/>create, update, transition]
Tools --> ConfTools[Confluence Tools<br/>create page, comment]
```
## Installation & Setup
For detailed installation and setup instructions, please refer to our [installation guide for AI assistants](./llms-install.md). This guide is specially formatted for AI/LLM assistants like Cline to read and automatically set up the MCP Atlassian Server.
> **Note for Cline users**: The installation guide (llms-install.md) is optimized for Cline AI to understand and execute. You can simply ask Cline to "Install MCP Atlassian Server (by phuc-nt)" and it will be able to parse the instructions and help you set up everything step-by-step.
The guide includes:
- Prerequisites and system requirements
- Step-by-step setup for Node.js environments
- Configuring Cline AI assistant to connect with Atlassian
- Getting and setting up Atlassian API tokens
- Security recommendations and best practices
### Installing via Smithery
To install Atlassian Integration Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@phuc-nt/mcp-atlassian-server):
```bash
npx -y @smithery/cli install @phuc-nt/mcp-atlassian-server --client claude
```
## Feature Overview
MCP Atlassian Server enables AI assistants (like Cline, Claude Desktop, Cursor...) to access and manage Jira & Confluence with a full set of features, grouped for clarity:
### Jira
- **Issue Management**
- View, search, and filter issues
- Create, update, transition, and assign issues
- Add issues to backlog or sprint, rank issues
- **Project Management**
- View project list, project details, and project roles
- **Board & Sprint Management**
- View boards, board configuration, issues and sprints on boards
- Create, start, and close sprints
- **Filter Management**
- View, create, update, and delete filters
- **Dashboard & Gadget Management**
- View dashboards and gadgets
- Create and update dashboards
- Add or remove gadgets on dashboards
- **User Management**
- View user details, assignable users, and users by project role
### Confluence
- **Space Management**
- View space list, space details, and pages in a space
- **Page Management**
- View, search, and get details of pages, child pages, ancestors, attachments, and version history
- Create, update, rename, and delete pages
- **Comment Management**
- View, add, update, and delete comments on pages
> For a full technical breakdown of all features, resources, and tools, see:
> [docs/introduction/resources-and-tools.md](./docs/introduction/resources-and-tools.md)
---
## Request Flow
```mermaid
sequenceDiagram
participant User
participant Cline as Cline AI
participant MCP as MCP Server
participant Atlassian as Atlassian API
User->>Cline: "Find all my assigned issues"
Cline->>MCP: Request jira://issues
MCP->>Atlassian: API Request with Auth
Atlassian->>MCP: JSON Response
MCP->>Cline: Formatted MCP Resource
Cline->>User: "I found these issues..."
User->>Cline: "Create new issue about login bug"
Cline->>MCP: Call createIssue Tool
MCP->>Atlassian: POST /rest/api/3/issue
Atlassian->>MCP: Created Issue Data
MCP->>Cline: Success Response
Cline->>User: "Created issue DEMO-123"
```
## Security Note
- Your API token inherits all permissions of the user that created it
- Never share your token with a non-trusted party
- Be cautious when asking LLMs to analyze config files containing your token
- See detailed security guidelines in [llms-install.md](./llms-install.md#security-warning-when-using-llms)
## Contribute & Support
- Contribute by opening Pull Requests or Issues on GitHub.
- Join the MCP/Cline community for additional support.
```
--------------------------------------------------------------------------------
/src/utils/jira-tool-api.ts:
--------------------------------------------------------------------------------
```typescript
export * from './jira-tool-api-v3.js';
export * from './jira-tool-api-agile.js';
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "dev_mcp-atlassian-test-client",
"version": "1.0.0",
"description": "Test client for MCP Atlassian server",
"type": "module",
"main": "dist/tool-test.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node --esm src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0"
},
"devDependencies": {
"@types/node": "^22.15.2",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
```
--------------------------------------------------------------------------------
/src/resources/confluence/index.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerSpaceResources } from './spaces.js';
import { registerPageResources } from './pages.js';
import { Logger } from '../../utils/logger.js';
const logger = Logger.getLogger('ConfluenceResources');
/**
* Register all Confluence resources with MCP Server
* @param server MCP Server instance
*/
export function registerConfluenceResources(server: McpServer) {
logger.info('Registering Confluence resources...');
// Register specific Confluence resources
registerSpaceResources(server);
registerPageResources(server);
logger.info('Confluence resources registered successfully');
}
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
version: '3.8'
services:
# MCP Server với STDIO transport
mcp-atlassian:
build:
context: .
dockerfile: Dockerfile
image: mcp-atlassian-server:latest
container_name: mcp-atlassian
environment:
- ATLASSIAN_SITE_NAME=${ATLASSIAN_SITE_NAME}
- ATLASSIAN_USER_EMAIL=${ATLASSIAN_USER_EMAIL}
- ATLASSIAN_API_TOKEN=${ATLASSIAN_API_TOKEN}
- MCP_SERVER_NAME=${MCP_SERVER_NAME:-mcp-atlassian-integration}
- MCP_SERVER_VERSION=${MCP_SERVER_VERSION:-1.0.0}
stdin_open: true # Cần thiết cho STDIO transport
tty: true # Cần thiết cho STDIO transport
volumes:
- ./.env:/app/.env:ro
restart: unless-stopped
```
--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerJiraResources } from './jira/index.js';
import { registerConfluenceResources } from './confluence/index.js';
import { Logger } from '../utils/logger.js';
const logger = Logger.getLogger('MCPResources');
/**
* Register all resources (Jira and Confluence) with MCP Server
* @param server MCP Server instance
*/
export function registerAllResources(server: McpServer) {
logger.info('Registering all MCP resources...');
// Register all Jira resources
registerJiraResources(server);
// Register all Confluence resources
registerConfluenceResources(server);
logger.info('All MCP resources registered successfully');
}
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
collectCoverage: true,
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.ts',
'!src/tests/**/*.ts',
],
testMatch: [
'**/src/tests/unit/**/*.test.ts',
'**/src/tests/integration/**/*.test.ts',
'**/src/tests/e2e/**/*.test.ts'
],
testPathIgnorePatterns: ['/node_modules/'],
verbose: true,
setupFilesAfterEnv: ['<rootDir>/src/tests/setup.ts'],
globals: {
'ts-jest': {
isolatedModules: true
}
}
};
```
--------------------------------------------------------------------------------
/src/resources/jira/index.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerIssueResources } from './issues.js';
import { registerProjectResources } from './projects.js';
import { registerUserResources } from './users.js';
import { registerFilterResources } from './filters.js';
import { registerBoardResources } from './boards.js';
import { registerSprintResources } from './sprints.js';
import { registerDashboardResources } from './dashboards.js';
import { registerGetJiraGadgetsResource } from '../../tools/jira/get-gadgets.js';
import { Logger } from '../../utils/logger.js';
const logger = Logger.getLogger('JiraResources');
/**
* Register all Jira resources with MCP Server
* @param server MCP Server instance
*/
export function registerJiraResources(server: McpServer) {
logger.info('Registering Jira resources...');
// Register specific Jira resources
registerIssueResources(server);
registerProjectResources(server);
registerUserResources(server);
registerFilterResources(server);
registerBoardResources(server);
registerSprintResources(server);
registerDashboardResources(server);
registerGetJiraGadgetsResource(server);
logger.info('Jira resources registered successfully');
}
```
--------------------------------------------------------------------------------
/src/utils/mcp-core.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Core interfaces and functions for MCP responses
* This module provides the foundation for all MCP responses
*/
/**
* Standard MCP response interface
*/
export interface McpResponse<T = any> {
contents: Array<McpContent>;
isError?: boolean;
data?: T;
[key: string]: unknown;
}
/**
* MCP content types
*/
export type McpContent =
{ uri: string; mimeType: string; text: string; [key: string]: unknown }
| { uri: string; mimeType: string; blob: string; [key: string]: unknown };
/**
* Create a standard response with JSON content
*/
export function createJsonResponse<T>(uri: string, data: T, mimeType = 'application/json'): McpResponse<T> {
return {
contents: [
{
uri,
mimeType,
text: JSON.stringify(data)
}
],
data
};
}
/**
* Create a standard success response
*/
export function createSuccessResponse(uri: string, message: string, data?: any): McpResponse {
return createJsonResponse(uri, {
success: true,
message,
...(data && { data })
});
}
/**
* Create a standard error response
*/
export function createErrorResponse(uri: string, message: string, details?: any): McpResponse {
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
success: false,
message,
...(details && { details })
})
}
],
isError: true
};
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@phuc-nt/mcp-atlassian-server",
"version": "2.1.1",
"description": "MCP Server for interacting with Atlassian Jira and Confluence",
"type": "module",
"main": "dist/index.js",
"files": [
"dist",
"README.md",
"LICENSE",
"assets"
],
"bin": {
"mcp-atlassian-server": "dist/index.js"
},
"scripts": {
"test": "jest",
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node --esm src/index.ts",
"prepublishOnly": "npm run build"
},
"keywords": [
"mcp",
"model",
"context",
"protocol",
"atlassian",
"jira",
"confluence"
],
"author": "Phuc Nguyen",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"axios": "^1.6.2",
"axios-retry": "^4.5.0",
"cross-fetch": "^4.1.0",
"dotenv": "^16.4.1",
"jira.js": "^3.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.11.13",
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/phuc-nt/mcp-atlassian-server.git"
},
"homepage": "https://github.com/phuc-nt/mcp-atlassian-server#readme",
"bugs": {
"url": "https://github.com/phuc-nt/mcp-atlassian-server/issues"
},
"engines": {
"node": ">=16.0.0"
},
"publishConfig": {
"access": "public"
}
}
```
--------------------------------------------------------------------------------
/src/tools/jira/get-gadgets.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { gadgetListSchema } from '../../schemas/jira.js';
import { getJiraAvailableGadgets } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:getGadgets');
export const registerGetJiraGadgetsResource = (server: McpServer) => {
server.resource(
'jira-gadgets-list',
new ResourceTemplate('jira://gadgets', {
list: async (_extra: any) => ({
resources: [
{
uri: 'jira://gadgets',
name: 'Jira Gadgets',
description: 'List all available Jira gadgets for dashboard.',
mimeType: 'application/json'
}
]
})
}),
async (uri: string | URL, params: Record<string, any>, extra: any) => {
try {
// Get config from context or environment
const config = Config.getConfigFromContextOrEnv(extra?.context);
const uriStr = typeof uri === 'string' ? uri : uri.href;
const gadgets = await getJiraAvailableGadgets(config);
return Resources.createStandardResource(
uriStr,
gadgets,
'gadgets',
gadgetListSchema,
gadgets.length,
gadgets.length,
0,
`${config.baseUrl}/jira/dashboards`
);
} catch (error) {
logger.error('Error in getJiraAvailableGadgets:', error);
throw error;
}
}
);
};
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- atlassianSiteName
- atlassianUserEmail
- atlassianApiToken
properties:
atlassianSiteName:
type: string
description: Your Atlassian site name or URL (e.g., example.atlassian.net or
https://example.atlassian.net)
atlassianUserEmail:
type: string
description: Email address associated with your Atlassian account
atlassianApiToken:
type: string
description: API token for Atlassian authentication
mcpServerName:
type: string
default: mcp-atlassian-integration
description: Optional custom name for the MCP server
mcpServerVersion:
type: string
default: 1.0.0
description: Optional custom version for the MCP server
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['dist/index.js'],
env: {
ATLASSIAN_SITE_NAME: config.atlassianSiteName,
ATLASSIAN_USER_EMAIL: config.atlassianUserEmail,
ATLASSIAN_API_TOKEN: config.atlassianApiToken,
MCP_SERVER_NAME: config.mcpServerName,
MCP_SERVER_VERSION: config.mcpServerVersion
}
})
exampleConfig:
atlassianSiteName: example.atlassian.net
atlassianUserEmail: [email protected]
atlassianApiToken: your-api-token
mcpServerName: my-atlassian-mcp
mcpServerVersion: 1.2.3
```
--------------------------------------------------------------------------------
/src/tools/jira/delete-filter.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { deleteFilter } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:deleteFilter');
// Input parameter schema
export const deleteFilterSchema = z.object({
filterId: z.string().describe('Filter ID to delete')
});
type DeleteFilterParams = z.infer<typeof deleteFilterSchema>;
async function deleteFilterToolImpl(params: DeleteFilterParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
logger.info(`Deleting filter with ID: ${params.filterId}`);
await deleteFilter(config, params.filterId);
return {
success: true,
filterId: params.filterId
};
}
// Register the tool with MCP Server
export const registerDeleteFilterTool = (server: McpServer) => {
server.tool(
'deleteFilter',
'Delete a filter in Jira',
deleteFilterSchema.shape,
async (params: DeleteFilterParams, context: Record<string, any>) => {
try {
const result = await deleteFilterToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in deleteFilter:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/close-sprint.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { closeSprint } from '../../utils/jira-tool-api-agile.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:closeSprint');
export const closeSprintSchema = z.object({
sprintId: z.string().describe('Sprint ID'),
completeDate: z.string().optional().describe('Complete date (ISO 8601, optional, e.g. 2025-05-10T12:45:00.000+07:00)')
});
type CloseSprintParams = z.infer<typeof closeSprintSchema>;
async function closeSprintToolImpl(params: CloseSprintParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { sprintId, ...options } = params;
const result = await closeSprint(config, sprintId, options);
return {
success: true,
sprintId,
...options,
result
};
}
export const registerCloseSprintTool = (server: McpServer) => {
server.tool(
'closeSprint',
'Close a Jira sprint',
closeSprintSchema.shape,
async (params: CloseSprintParams, context: Record<string, any>) => {
try {
const result = await closeSprintToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in closeSprint:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/create-dashboard.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createDashboard } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:createDashboard');
export const createDashboardSchema = z.object({
name: z.string().describe('Dashboard name'),
description: z.string().optional().describe('Dashboard description'),
sharePermissions: z.array(z.any()).optional().describe('Share permissions array')
});
type CreateDashboardParams = z.infer<typeof createDashboardSchema>;
async function createDashboardToolImpl(params: CreateDashboardParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const result = await createDashboard(config, params);
return {
success: true,
...result
};
}
export const registerCreateDashboardTool = (server: McpServer) => {
server.tool(
'createDashboard',
'Create a new Jira dashboard',
createDashboardSchema.shape,
async (params: CreateDashboardParams, context: Record<string, any>) => {
try {
const result = await createDashboardToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in createDashboard:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# CHANGELOG
## [2.1.1] - 2025-05-17
### 📝 Patch Release
- Documentation and metadata updates only. No code changes.
## [2.1.0] - 2025-05-17
### ✨ Refactor & Standardization
- Refactored the entire codebase to standardize resource/tool structure
- Completely removed the content-metadata resource, merged metadata into the page resource
- Updated and standardized developer documentation for easier extension and maintenance
- Ensured compatibility with the latest MCP SDK, improved security, scalability, and maintainability
- Updated `docs/introduction/resources-and-tools.md` to remove all references to content-metadata
### 🔧 Bug Fixes
- Fixed duplicate resource registration issues
- Improved resource management and registration process
- Resolved issues with conflicting resource patterns
## [2.0.0] - 2025-05-11
### ✨ Improvements
- Updated all APIs to latest versions (Jira API v3, Confluence API v2)
- Improved documentation and README structure
- Reorganized resources and tools into logical groups
### 🎉 New Features
- **Jira Board & Sprint:** Management of boards, sprints, and issues for Agile/Scrum workflows
- **Jira Dashboard & Gadgets:** Create/update dashboards, add/remove gadgets
- **Jira Filters:** Create, view, update, delete search filters for issues
- **Advanced Confluence Pages:** Version management, attachments, page deletion
- **Confluence Comments:** Update and delete comments
- Expanded from 21 to 48 features, including numerous new tools for both Jira and Confluence
### 🔧 Bug Fixes
- Fixed issues with Jira dashboard and gadget tools/resources
- Resolved problems with jira://users resource
- Improved error handling and messaging
- Fixed compatibility issues between API versions
### 🔄 Code Changes
- Restructured codebase for easier future expansion
- Improved feature implementation workflow
```
--------------------------------------------------------------------------------
/src/tools/jira/start-sprint.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { startSprint } from '../../utils/jira-tool-api-agile.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:startSprint');
export const startSprintSchema = z.object({
sprintId: z.string().describe('Sprint ID'),
startDate: z.string().describe('Start date (ISO 8601)'),
endDate: z.string().describe('End date (ISO 8601)'),
goal: z.string().optional().describe('Sprint goal')
});
type StartSprintParams = z.infer<typeof startSprintSchema>;
async function startSprintToolImpl(params: StartSprintParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { sprintId, startDate, endDate, goal } = params;
const result = await startSprint(config, sprintId, startDate, endDate, goal);
return {
success: true,
sprintId,
startDate,
endDate,
goal: goal || null,
result
};
}
export const registerStartSprintTool = (server: McpServer) => {
server.tool(
'startSprint',
'Start a Jira sprint',
startSprintSchema.shape,
async (params: StartSprintParams, context: Record<string, any>) => {
try {
const result = await startSprintToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in startSprint:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/remove-gadget-from-dashboard.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { removeGadgetFromDashboard } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:removeGadgetFromDashboard');
export const removeGadgetFromDashboardSchema = z.object({
dashboardId: z.string().describe('Dashboard ID'),
gadgetId: z.string().describe('Gadget ID')
});
type RemoveGadgetFromDashboardParams = z.infer<typeof removeGadgetFromDashboardSchema>;
async function removeGadgetFromDashboardToolImpl(params: RemoveGadgetFromDashboardParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { dashboardId, gadgetId } = params;
const result = await removeGadgetFromDashboard(config, dashboardId, gadgetId);
return {
success: true,
dashboardId,
gadgetId,
result
};
}
export const registerRemoveGadgetFromDashboardTool = (server: McpServer) => {
server.tool(
'removeGadgetFromDashboard',
'Remove gadget from Jira dashboard',
removeGadgetFromDashboardSchema.shape,
async (params: RemoveGadgetFromDashboardParams, context: Record<string, any>) => {
try {
const result = await removeGadgetFromDashboardToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in removeGadgetFromDashboard:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/update-dashboard.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { updateDashboard } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:updateDashboard');
export const updateDashboardSchema = z.object({
dashboardId: z.string().describe('Dashboard ID'),
name: z.string().optional().describe('Dashboard name'),
description: z.string().optional().describe('Dashboard description'),
sharePermissions: z.array(z.any()).optional().describe('Share permissions array')
});
type UpdateDashboardParams = z.infer<typeof updateDashboardSchema>;
async function updateDashboardToolImpl(params: UpdateDashboardParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { dashboardId, ...data } = params;
const result = await updateDashboard(config, dashboardId, data);
return {
success: true,
dashboardId,
...result
};
}
export const registerUpdateDashboardTool = (server: McpServer) => {
server.tool(
'updateDashboard',
'Update a Jira dashboard',
updateDashboardSchema.shape,
async (params: UpdateDashboardParams, context: Record<string, any>) => {
try {
const result = await updateDashboardToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in updateDashboard:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/add-issue-to-sprint.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { addIssueToSprint } from '../../utils/jira-tool-api-agile.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:addIssueToSprint');
export const addIssueToSprintSchema = z.object({
sprintId: z.string().describe('Target sprint ID (must be future or active)'),
issueKeys: z.array(z.string()).min(1).max(50).describe('List of issue keys to move to the sprint (max 50)')
});
type AddIssueToSprintParams = z.infer<typeof addIssueToSprintSchema>;
async function addIssueToSprintToolImpl(params: AddIssueToSprintParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { sprintId, issueKeys } = params;
const result = await addIssueToSprint(config, sprintId, issueKeys);
return {
success: true,
sprintId,
issueKeys,
result
};
}
export const registerAddIssueToSprintTool = (server: McpServer) => {
server.tool(
'addIssueToSprint',
'Add issues to a Jira sprint (POST /rest/agile/1.0/sprint/{sprintId}/issue)',
addIssueToSprintSchema.shape,
async (params: AddIssueToSprintParams, context: Record<string, any>) => {
try {
const result = await addIssueToSprintToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in addIssueToSprint:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/docs/dev-guide/marketplace-publish-application-template.md:
--------------------------------------------------------------------------------
```markdown
# MCP Server Submission Template
## [Server Submission]: {SERVER_NAME}
**GitHub Repo URL:**
{FULL_GITHUB_REPOSITORY_URL}
**Logo Image:**
{ATTACH_400x400_PNG_LOGO_HERE}
**Reason for Addition:**
{1-2 SENTENCES EXPLAINING WHAT THIS SERVER DOES AND WHY IT'S VALUABLE}
### Key Features
- {FEATURE_1}
- {FEATURE_2}
- {FEATURE_3}
- {FEATURE_4}
- {FEATURE_5}
### Use Cases
- {USE_CASE_1}
- {USE_CASE_2}
- {USE_CASE_3}
- {USE_CASE_4}
### Installation
{1-2 SENTENCES ABOUT INSTALLATION PROCESS AND DOCUMENTATION}
### Requirements
- {REQUIREMENT_1}
- {REQUIREMENT_2}
- {REQUIREMENT_3}
- {REQUIREMENT_4}
{OPTIONAL_ADDITIONAL_NOTES}
---
## Example Filled Template
## [Server Submission]: MCP Atlassian Server
**GitHub Repo URL:**
https://github.com/username/mcp-atlassian-server
**Logo Image:**
[Atlassian MCP Server Logo.png]
**Reason for Addition:**
This MCP server connects AI assistants to Atlassian Jira & Confluence, enabling natural language interaction with project management tools. It helps users manage issues, projects, and documentation without switching contexts.
### Key Features
- Connects AI assistants to Jira and Confluence Cloud
- Supports both Resources (read-only data) and Tools (actions)
- Local-first design for privacy and performance
- Thoroughly tested with Cline
- Detailed documentation for easy installation
### Use Cases
- Query Jira issues, projects, and user information
- Create and update issues, transition states, assign tasks
- Create Confluence pages and add comments
- Generate reports and summaries from Atlassian data
### Installation
The server includes comprehensive installation instructions in README.md and a detailed llms-install.md specifically designed for AI-assisted installation.
### Requirements
- Node.js 16+ and npm
- Atlassian Cloud account with API token
- Compatible with Cline and other MCP clients
This server follows MCP best practices and is ready for one-click installation via Marketplace.
```
--------------------------------------------------------------------------------
/src/tools/jira/update-filter.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../../utils/logger.js';
import { updateFilter } from '../../utils/jira-tool-api-v3.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:updateFilter');
// Input parameter schema
export const updateFilterSchema = z.object({
filterId: z.string().describe('Filter ID to update'),
name: z.string().optional().describe('New filter name'),
jql: z.string().optional().describe('New JQL query'),
description: z.string().optional().describe('New description'),
favourite: z.boolean().optional().describe('Mark as favourite')
});
type UpdateFilterParams = z.infer<typeof updateFilterSchema>;
async function updateFilterToolImpl(params: UpdateFilterParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
logger.info(`Updating filter with ID: ${params.filterId}`);
const response = await updateFilter(config, params.filterId, params);
return {
id: response.id,
name: response.name,
self: response.self,
success: true
};
}
// Register the tool with MCP Server
export const registerUpdateFilterTool = (server: McpServer) => {
server.tool(
'updateFilter',
'Update an existing filter in Jira',
updateFilterSchema.shape,
async (params: UpdateFilterParams, context: Record<string, any>) => {
try {
const result = await updateFilterToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in updateFilter:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/docs/introduction/marketplace-submission.md:
--------------------------------------------------------------------------------
```markdown
# MCP Server Submission Template
## [Server Submission]: MCP Atlassian Server (by phuc-nt)
**GitHub Repo URL:**
https://github.com/phuc-nt/mcp-atlassian-server
**Logo Image:**

**Reason for Addition:**
This MCP server connects AI assistants to Atlassian Jira & Confluence, enabling natural language interaction with project management tools. It helps users manage issues, projects, and documentation without switching contexts during deep work.
### Key Features
- Connects AI assistants to Atlassian Jira and Confluence
- Provides both Resources (read-only data) and Tools (action endpoints)
- Local-first design for personal development environments
- Optimized for integration with Cline AI assistant
- Comprehensive documentation for users and developers
### Use Cases
- Query Jira issues, projects, and user information directly through AI
- Create and update issues, transition states, assign tasks seamlessly
- Create Confluence pages and add comments without context-switching
- Generate reports and summaries from Atlassian data using natural language
- Manage your daily Atlassian workflows through conversational interfaces
### Installation
The server includes comprehensive installation instructions in README.md and a detailed llms-install.md specifically designed for AI-assisted installation. Users can simply ask Cline to "Install MCP Atlassian Server (by phuc-nt)" for one-click setup.
### Requirements
- Node.js 16+ and npm
- Atlassian Cloud account with API token
- Compatible with Cline and other MCP-compatible clients
### Additional Notes
This server follows MCP best practices and is ready for one-click installation via Marketplace. It aims to enhance developer productivity by eliminating the need to context-switch between development environments and Atlassian tools. The project is actively maintained with a clear roadmap for future enhancements.
---
*This submission is intended for the Cline MCP Marketplace and follows the specified template format.*
```
--------------------------------------------------------------------------------
/src/tools/jira/create-filter.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Create Filter Tool
*
* This tool creates a new filter in Jira.
*/
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createFilter } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:createFilter');
// Input parameter schema
export const createFilterSchema = z.object({
name: z.string().describe('Filter name'),
jql: z.string().describe('JQL query for the filter'),
description: z.string().optional().describe('Filter description'),
favourite: z.boolean().optional().describe('Mark as favourite')
});
type CreateFilterParams = z.infer<typeof createFilterSchema>;
async function createFilterToolImpl(params: CreateFilterParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
logger.info(`Creating filter: ${params.name}`);
const response = await createFilter(config, params.name, params.jql, params.description, params.favourite);
return {
id: response.id,
name: response.name,
self: response.self,
success: true
};
}
// Register the tool with MCP Server
export const registerCreateFilterTool = (server: McpServer) => {
server.tool(
'createFilter',
'Create a new filter in Jira',
createFilterSchema.shape,
async (params: CreateFilterParams, context: Record<string, any>) => {
try {
const result = await createFilterToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in createFilter:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/create-sprint.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../../utils/logger.js';
import { createSprint } from '../../utils/jira-tool-api-agile.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:createSprint');
// Input parameter schema
export const createSprintSchema = z.object({
boardId: z.string().describe('Board ID'),
name: z.string().describe('Sprint name'),
startDate: z.string().optional().describe('Start date (ISO format)'),
endDate: z.string().optional().describe('End date (ISO format)'),
goal: z.string().optional().describe('Sprint goal')
});
type CreateSprintParams = z.infer<typeof createSprintSchema>;
async function createSprintToolImpl(params: CreateSprintParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
logger.info(`Creating sprint: ${params.name} for board ${params.boardId}`);
const response = await createSprint(config, params.boardId, params.name, params.startDate, params.endDate, params.goal);
return {
id: response.id,
name: response.name,
state: response.state,
success: true
};
}
// Register the tool with MCP Server
export const registerCreateSprintTool = (server: McpServer) => {
server.tool(
'createSprint',
'Create a new sprint in Jira',
createSprintSchema.shape,
async (params: CreateSprintParams, context: Record<string, any>) => {
try {
const result = await createSprintToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in createSprint:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/rank-backlog-issues.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { rankBacklogIssues } from '../../utils/jira-tool-api-agile.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:rankBacklogIssues');
export const rankBacklogIssuesSchema = z.object({
boardId: z.string().describe('Board ID'),
issueKeys: z.array(z.string()).describe('List of issue keys to rank'),
rankBeforeIssue: z.string().optional().describe('Rank before this issue key'),
rankAfterIssue: z.string().optional().describe('Rank after this issue key')
});
type RankBacklogIssuesParams = z.infer<typeof rankBacklogIssuesSchema>;
async function rankBacklogIssuesToolImpl(params: RankBacklogIssuesParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { boardId, issueKeys, rankBeforeIssue, rankAfterIssue } = params;
const result = await rankBacklogIssues(config, boardId, issueKeys, { rankBeforeIssue, rankAfterIssue });
return {
success: true,
boardId,
issueKeys,
rankBeforeIssue: rankBeforeIssue || null,
rankAfterIssue: rankAfterIssue || null,
result
};
}
export const registerRankBacklogIssuesTool = (server: McpServer) => {
server.tool(
'rankBacklogIssues',
'Rank issues in Jira backlog',
rankBacklogIssuesSchema.shape,
async (params: RankBacklogIssuesParams, context: Record<string, any>) => {
try {
const result = await rankBacklogIssuesToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in rankBacklogIssues:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/add-issues-to-backlog.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { addIssuesToBacklog } from '../../utils/jira-tool-api-agile.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:addIssuesToBacklog');
export const addIssuesToBacklogSchema = z.object({
issueKeys: z.union([
z.string(),
z.array(z.string())
]).describe('Issue key(s) to move to backlog. Accepts a single issue key (e.g. "PROJ-123") or an array of issue keys (e.g. ["PROJ-123", "PROJ-124"]).'),
boardId: z.string().optional().describe('Board ID (optional). If provided, issues will be moved to the backlog of this board.')
});
type AddIssuesToBacklogParams = z.infer<typeof addIssuesToBacklogSchema>;
async function addIssuesToBacklogToolImpl(params: AddIssuesToBacklogParams, context: any) {
const config = Config.getConfigFromContextOrEnv(context);
const { boardId, issueKeys } = params;
const keys = Array.isArray(issueKeys) ? issueKeys : [issueKeys];
const result = await addIssuesToBacklog(config, keys, boardId);
return {
success: true,
boardId: boardId || null,
issueKeys: keys,
result
};
}
export const registerAddIssuesToBacklogTool = (server: McpServer) => {
server.tool(
'addIssuesToBacklog',
'Move issue(s) to Jira backlog (POST /rest/agile/1.0/backlog/issue or /rest/agile/1.0/backlog/{boardId}/issue)',
addIssuesToBacklogSchema.shape,
async (params: AddIssuesToBacklogParams, context: Record<string, any>) => {
try {
const result = await addIssuesToBacklogToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in addIssuesToBacklog:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/assign-issue.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { assignIssue } from '../../utils/jira-tool-api-v3.js';
import { ApiError } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:assignIssue');
// Input parameter schema
export const assignIssueSchema = z.object({
issueIdOrKey: z.string().describe('ID or key of the issue (e.g., PROJ-123)'),
accountId: z.string().optional().describe('Account ID of the assignee (leave blank to unassign)')
});
type AssignIssueParams = z.infer<typeof assignIssueSchema>;
async function assignIssueToolImpl(params: AssignIssueParams, context: any) {
const config: AtlassianConfig = Config.getConfigFromContextOrEnv(context);
logger.info(`Assigning issue ${params.issueIdOrKey} to ${params.accountId || 'no one'}`);
const result = await assignIssue(
config,
params.issueIdOrKey,
params.accountId || null
);
return {
issueIdOrKey: params.issueIdOrKey,
success: result.success,
assignee: params.accountId || null,
message: params.accountId
? `Issue ${params.issueIdOrKey} assigned to user with account ID: ${params.accountId}`
: `Issue ${params.issueIdOrKey} unassigned`
};
}
export const registerAssignIssueTool = (server: McpServer) => {
server.tool(
'assignIssue',
'Assign a Jira issue to a user',
assignIssueSchema.shape,
async (params: AssignIssueParams, context: Record<string, any>) => {
try {
const result = await assignIssueToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/transition-issue.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { transitionIssue } from '../../utils/jira-tool-api-v3.js';
import { ApiError } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:transitionIssue');
// Input parameter schema
export const transitionIssueSchema = z.object({
issueIdOrKey: z.string().describe('ID or key of the issue (e.g., PROJ-123)'),
transitionId: z.string().describe('ID of the transition to apply'),
comment: z.string().optional().describe('Comment when performing the transition')
});
type TransitionIssueParams = z.infer<typeof transitionIssueSchema>;
async function transitionIssueToolImpl(params: TransitionIssueParams, context: any) {
const config: AtlassianConfig = Config.getConfigFromContextOrEnv(context);
logger.info(`Transitioning issue ${params.issueIdOrKey} with transition ${params.transitionId}`);
const result = await transitionIssue(
config,
params.issueIdOrKey,
params.transitionId,
params.comment
);
return {
issueIdOrKey: params.issueIdOrKey,
success: result.success,
transitionId: params.transitionId,
message: result.message
};
}
// Register the tool with MCP Server
export const registerTransitionIssueTool = (server: McpServer) => {
server.tool(
'transitionIssue',
'Transition the status of a Jira issue',
transitionIssueSchema.shape,
async (params: TransitionIssueParams, context: Record<string, any>) => {
try {
const result = await transitionIssueToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/start-docker.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Banner
echo -e "${BLUE}"
echo "====================================================="
echo " MCP Atlassian Server Docker Manager"
echo "====================================================="
echo -e "${NC}"
# Kiểm tra xem file .env tồn tại
if [ ! -f .env ]; then
echo -e "${RED}Lỗi: File .env không tồn tại.${NC}"
echo "Vui lòng tạo file .env với các biến môi trường sau:"
echo "ATLASSIAN_SITE_NAME=your-site.atlassian.net"
echo "[email protected]"
echo "ATLASSIAN_API_TOKEN=your-api-token"
exit 1
fi
# Hiển thị menu
echo -e "${YELLOW}Chọn chế độ quản lý MCP Server:${NC}"
echo "1. Chạy MCP Server (với STDIO Transport)"
echo "2. Dừng và xóa container"
echo "3. Xem logs của container"
echo "4. Hiển thị cấu hình Cline"
echo "5. Thoát"
echo ""
read -p "Nhập lựa chọn của bạn (1-5): " choice
case $choice in
1)
echo -e "${GREEN}Chạy MCP Server với STDIO Transport...${NC}"
docker compose up --build -d mcp-atlassian
echo -e "${GREEN}MCP Server đã được khởi động với STDIO transport.${NC}"
echo -e "${YELLOW}Hướng dẫn cấu hình Cline:${NC}"
echo "1. Mở Cline trong VS Code"
echo "2. Đi đến cấu hình MCP Servers"
echo "3. Thêm cấu hình sau vào file cline_mcp_settings.json:"
echo ""
echo '{
"mcpServers": {
"atlassian-docker": {
"command": "docker",
"args": ["exec", "-i", "mcp-atlassian", "node", "dist/index.js"],
"env": {}
}
}
}'
;;
2)
echo -e "${YELLOW}Dừng và xóa container hiện tại...${NC}"
docker compose down
echo -e "${GREEN}Đã dừng và xóa các container MCP Server.${NC}"
;;
3)
echo -e "${YELLOW}Container đang chạy:${NC}"
docker ps --filter "name=mcp-atlassian"
echo ""
echo -e "${GREEN}Hiển thị logs của container mcp-atlassian...${NC}"
docker logs -f mcp-atlassian
;;
4)
echo -e "${GREEN}Cấu hình Cline để kết nối với MCP Server:${NC}"
echo '{
"mcpServers": {
"atlassian-docker": {
"command": "docker",
"args": ["exec", "-i", "mcp-atlassian", "node", "dist/index.js"],
"env": {}
}
}
}'
;;
5)
echo -e "${GREEN}Thoát chương trình.${NC}"
exit 0
;;
*)
echo -e "${RED}Lựa chọn không hợp lệ.${NC}"
exit 1
;;
esac
```
--------------------------------------------------------------------------------
/src/schemas/common.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Common schema definitions and metadata utilities for MCP resources
*/
/**
* Standard metadata interface for all resources
*/
export interface StandardMetadata {
total: number; // Total number of records
limit: number; // Maximum number of records returned
offset: number; // Starting position
hasMore: boolean; // Whether there are more records
links?: { // Useful links
self: string; // Link to this resource
ui?: string; // Link to Atlassian UI
next?: string; // Link to next page
}
}
/**
* Creates standard metadata object for resource responses
*/
export function createStandardMetadata(
total: number,
limit: number,
offset: number,
baseUrl: string,
uiUrl?: string
): StandardMetadata {
const hasMore = offset + limit < total;
// Base metadata
const metadata: StandardMetadata = {
total,
limit,
offset,
hasMore,
links: {
self: baseUrl
}
};
// Add UI link if provided
if (uiUrl) {
metadata.links!.ui = uiUrl;
}
// Add next page link if there are more results
if (hasMore) {
// Parse the current URL
try {
const url = new URL(baseUrl);
url.searchParams.set('offset', String(offset + limit));
url.searchParams.set('limit', String(limit));
metadata.links!.next = url.toString();
} catch (error) {
// If URL parsing fails, construct a simple next link
const separator = baseUrl.includes('?') ? '&' : '?';
metadata.links!.next = `${baseUrl}${separator}offset=${offset + limit}&limit=${limit}`;
}
}
return metadata;
}
/**
* JSON Schema definition for standard metadata
*/
export const standardMetadataSchema = {
type: "object",
properties: {
total: { type: "number", description: "Total number of records" },
limit: { type: "number", description: "Maximum number of records returned" },
offset: { type: "number", description: "Starting position" },
hasMore: { type: "boolean", description: "Whether there are more records" },
links: {
type: "object",
properties: {
self: { type: "string", description: "Link to this resource" },
ui: { type: "string", description: "Link to Atlassian UI" },
next: { type: "string", description: "Link to next page" }
}
}
},
required: ["total", "limit", "offset", "hasMore"]
};
```
--------------------------------------------------------------------------------
/src/tools/jira/create-issue.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { createIssue } from '../../utils/jira-tool-api-v3.js';
import { ApiError } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:createIssue');
// Input parameter schema
export const createIssueSchema = z.object({
projectKey: z.string().describe('Project key (e.g., PROJ)'),
summary: z.string().describe('Issue summary'),
issueType: z.string().default('Task').describe('Issue type (e.g., Bug, Task, Story)'),
description: z.string().optional().describe('Issue description'),
priority: z.string().optional().describe('Priority (e.g., High, Medium, Low)'),
assignee: z.string().optional().describe('Assignee username'),
labels: z.array(z.string()).optional().describe('Labels for the issue')
});
type CreateIssueParams = z.infer<typeof createIssueSchema>;
async function createIssueToolImpl(params: CreateIssueParams, context: any) {
const config: AtlassianConfig = Config.getConfigFromContextOrEnv(context);
logger.info(`Creating new issue in project: ${params.projectKey}`);
const additionalFields: Record<string, any> = {};
if (params.priority) {
additionalFields.priority = { name: params.priority };
}
if (params.assignee) {
additionalFields.assignee = { name: params.assignee };
}
if (params.labels && params.labels.length > 0) {
additionalFields.labels = params.labels;
}
const newIssue = await createIssue(
config,
params.projectKey,
params.summary,
params.description,
params.issueType,
additionalFields
);
return {
id: newIssue.id,
key: newIssue.key,
self: newIssue.self,
success: true
};
}
export const registerCreateIssueTool = (server: McpServer) => {
server.tool(
'createIssue',
'Create a new issue in Jira',
createIssueSchema.shape,
async (params: CreateIssueParams, context: Record<string, any>) => {
try {
const result = await createIssueToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Define log levels
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3
}
// Define colors for output
const COLORS = {
RESET: '\x1b[0m',
RED: '\x1b[31m',
YELLOW: '\x1b[33m',
BLUE: '\x1b[34m',
GRAY: '\x1b[90m'
};
// Get log level from environment variable
const getLogLevelFromEnv = (): LogLevel => {
const logLevel = process.env.LOG_LEVEL?.toLowerCase();
switch (logLevel) {
case 'debug':
return LogLevel.DEBUG;
case 'info':
return LogLevel.INFO;
case 'warn':
return LogLevel.WARN;
case 'error':
return LogLevel.ERROR;
default:
return LogLevel.INFO; // Default is INFO
}
};
/**
* Logger utility
*/
export class Logger {
private static logLevel = getLogLevelFromEnv();
private moduleName: string;
/**
* Initialize logger
* @param moduleName Module name using the logger
*/
constructor(moduleName: string) {
this.moduleName = moduleName;
}
/**
* Log error
* @param message Log message
* @param data Additional data (optional)
*/
error(message: string, data?: any): void {
if (Logger.logLevel >= LogLevel.ERROR) {
console.error(`${COLORS.RED}[ERROR][${this.moduleName}]${COLORS.RESET} ${message}`);
if (data) console.error(data);
}
}
/**
* Log warning
* @param message Log message
* @param data Additional data (optional)
*/
warn(message: string, data?: any): void {
if (Logger.logLevel >= LogLevel.WARN) {
console.warn(`${COLORS.YELLOW}[WARN][${this.moduleName}]${COLORS.RESET} ${message}`);
if (data) console.warn(data);
}
}
/**
* Log info
* @param message Log message
* @param data Additional data (optional)
*/
info(message: string, data?: any): void {
if (Logger.logLevel >= LogLevel.INFO) {
console.info(`${COLORS.BLUE}[INFO][${this.moduleName}]${COLORS.RESET} ${message}`);
if (data) console.info(data);
}
}
/**
* Log debug
* @param message Log message
* @param data Additional data (optional)
*/
debug(message: string, data?: any): void {
if (Logger.logLevel >= LogLevel.DEBUG) {
console.debug(`${COLORS.GRAY}[DEBUG][${this.moduleName}]${COLORS.RESET} ${message}`);
if (data) console.debug(data);
}
}
/**
* Create a logger instance
* @param moduleName Module name using the logger
* @returns Logger instance
*/
static getLogger(moduleName: string): Logger {
return new Logger(moduleName);
}
/**
* Set log level
* @param level New log level
*/
static setLogLevel(level: LogLevel): void {
Logger.logLevel = level;
}
}
```
--------------------------------------------------------------------------------
/src/tests/e2e/mcp-server.test.ts:
--------------------------------------------------------------------------------
```typescript
import { McpClient } from '@modelcontextprotocol/sdk/client/mcp.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { InMemoryClientServerPair } from '@modelcontextprotocol/sdk/server/memory.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { registerGetIssueTool } from '../../tools/jira/get-issue.js';
import { registerSearchIssuesTool } from '../../tools/jira/search-issues.js';
import { registerGetPageTool } from '../../tools/confluence/get-page.js';
import { registerGetSpacesTool } from '../../tools/confluence/get-spaces.js';
import dotenv from 'dotenv';
// Tải biến môi trường
dotenv.config();
describe('MCP Server E2E Tests', () => {
let server: McpServer;
let client: McpClient;
let testConfig: AtlassianConfig;
beforeEach(() => {
// Thiết lập cấu hình test từ biến môi trường
const ATLASSIAN_SITE_NAME = process.env.ATLASSIAN_SITE_NAME || 'test-site';
const ATLASSIAN_USER_EMAIL = process.env.ATLASSIAN_USER_EMAIL || '[email protected]';
const ATLASSIAN_API_TOKEN = process.env.ATLASSIAN_API_TOKEN || 'test-token';
testConfig = {
baseUrl: `https://${ATLASSIAN_SITE_NAME}.atlassian.net`,
email: ATLASSIAN_USER_EMAIL,
apiToken: ATLASSIAN_API_TOKEN
};
// Tạo cặp server-client cho test
const pair = new InMemoryClientServerPair();
server = new McpServer({
name: 'mcp-atlassian-test-server',
version: '1.0.0'
});
client = new McpClient();
// Kết nối server và client
server.connect(pair.serverTransport);
client.connect(pair.clientTransport);
// Đăng ký context cho mỗi tool handler
const context = new Map<string, any>();
context.set('atlassianConfig', testConfig);
// Đăng ký một số tools để test
registerGetIssueTool(server);
registerSearchIssuesTool(server);
registerGetPageTool(server);
registerGetSpacesTool(server);
});
afterEach(() => {
// Đóng kết nối sau mỗi test
server.close();
client.close();
});
test('Server should register tools correctly', async () => {
// Lấy danh sách tools đã đăng ký
const tools = await client.getToolList();
// Kiểm tra số lượng tools đã đăng ký
expect(tools.length).toBeGreaterThan(0);
// Kiểm tra các tools cụ thể
const toolNames = tools.map(tool => tool.name);
expect(toolNames).toContain('getIssue');
expect(toolNames).toContain('searchIssues');
expect(toolNames).toContain('getPage');
expect(toolNames).toContain('getSpaces');
});
// Thêm các test case cho tool calls sẽ được bổ sung sau
// khi hoàn thiện việc cập nhật API các tools
test.todo('Should call getIssue tool successfully');
test.todo('Should call searchIssues tool successfully');
test.todo('Should handle error cases properly');
});
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
import { registerCreateIssueTool } from './jira/create-issue.js';
import { registerUpdateIssueTool } from './jira/update-issue.js';
import { registerTransitionIssueTool } from './jira/transition-issue.js';
import { registerAssignIssueTool } from './jira/assign-issue.js';
import { registerCreateFilterTool } from './jira/create-filter.js';
import { registerUpdateFilterTool } from './jira/update-filter.js';
import { registerDeleteFilterTool } from './jira/delete-filter.js';
import { registerCreateSprintTool } from './jira/create-sprint.js';
import { registerCreatePageTool } from './confluence/create-page.js';
import { registerUpdatePageTool } from './confluence/update-page.js';
import { registerAddCommentTool } from './confluence/add-comment.js';
import { registerDeletePageTool } from './confluence/delete-page.js';
import { registerUpdatePageTitleTool } from './confluence/update-page-title.js';
import { registerUpdateFooterCommentTool } from './confluence/update-footer-comment.js';
import { registerDeleteFooterCommentTool } from './confluence/delete-footer-comment.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerStartSprintTool } from './jira/start-sprint.js';
import { registerCloseSprintTool } from './jira/close-sprint.js';
import { registerAddIssuesToBacklogTool } from './jira/add-issues-to-backlog.js';
import { registerRankBacklogIssuesTool } from './jira/rank-backlog-issues.js';
import { registerCreateDashboardTool } from './jira/create-dashboard.js';
import { registerUpdateDashboardTool } from './jira/update-dashboard.js';
import { registerAddGadgetToDashboardTool } from './jira/add-gadget-to-dashboard.js';
import { registerRemoveGadgetFromDashboardTool } from './jira/remove-gadget-from-dashboard.js';
import { registerAddIssueToSprintTool } from './jira/add-issue-to-sprint.js';
/**
* Register all tools with MCP Server
* @param server MCP Server instance
*/
export function registerAllTools(server: McpServer) {
// Jira issue tools
registerCreateIssueTool(server);
registerUpdateIssueTool(server);
registerTransitionIssueTool(server);
registerAssignIssueTool(server);
// Jira filter tools
registerCreateFilterTool(server);
registerUpdateFilterTool(server);
registerDeleteFilterTool(server);
// Jira sprint tools
registerCreateSprintTool(server);
registerStartSprintTool(server);
registerCloseSprintTool(server);
// Jira board tools
// registerAddIssueToBoardTool(server);
// Jira backlog tools
registerAddIssuesToBacklogTool(server);
registerRankBacklogIssuesTool(server);
// Jira dashboard/gadget tools
registerCreateDashboardTool(server);
registerUpdateDashboardTool(server);
registerAddGadgetToDashboardTool(server);
registerRemoveGadgetFromDashboardTool(server);
// Confluence tools
registerCreatePageTool(server);
registerUpdatePageTool(server);
registerAddCommentTool(server);
registerDeletePageTool(server);
registerUpdatePageTitleTool(server);
registerUpdateFooterCommentTool(server);
registerDeleteFooterCommentTool(server);
registerAddIssueToSprintTool(server);
}
```
--------------------------------------------------------------------------------
/src/tools/jira/update-issue.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { updateIssue } from '../../utils/jira-tool-api-v3.js';
import { ApiError } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('JiraTools:updateIssue');
// Input parameter schema
export const updateIssueSchema = z.object({
issueIdOrKey: z.string().describe('ID or key of the issue to update (e.g., PROJ-123)'),
summary: z.string().optional().describe('New summary of the issue'),
description: z.string().optional().describe('New description of the issue'),
priority: z.string().optional().describe('New priority (e.g., High, Medium, Low)'),
labels: z.array(z.string()).optional().describe('New labels for the issue'),
customFields: z.record(z.any()).optional().describe('Custom fields to update')
});
type UpdateIssueParams = z.infer<typeof updateIssueSchema>;
async function updateIssueToolImpl(params: UpdateIssueParams, context: any) {
const config: AtlassianConfig = Config.getConfigFromContextOrEnv(context);
logger.info(`Updating issue: ${params.issueIdOrKey}`);
const fields: Record<string, any> = {};
if (params.summary) {
fields.summary = params.summary;
}
if (params.description) {
fields.description = {
version: 1,
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: params.description
}
]
}
]
};
}
if (params.priority) {
fields.priority = { name: params.priority };
}
if (params.labels) {
fields.labels = params.labels;
}
if (params.customFields) {
Object.entries(params.customFields).forEach(([key, value]) => {
fields[key] = value;
});
}
if (Object.keys(fields).length === 0) {
return {
issueIdOrKey: params.issueIdOrKey,
success: false,
message: 'No fields provided to update'
};
}
const result = await updateIssue(
config,
params.issueIdOrKey,
fields
);
return {
issueIdOrKey: params.issueIdOrKey,
success: result.success,
message: result.message
};
}
export const registerUpdateIssueTool = (server: McpServer) => {
server.tool(
'updateIssue',
'Update information of a Jira issue',
updateIssueSchema.shape,
async (params: UpdateIssueParams, context: Record<string, any>) => {
try {
const result = await updateIssueToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/docs/knowledge/03-mcp-prompts-sampling.md:
--------------------------------------------------------------------------------
```markdown
# MCP Server: Prompting and Sampling Techniques
This document provides guidance on effectively using MCP resources and tools with AI models, focusing on prompt design and sampling strategies. It covers best practices for integrating MCP with LLMs to maximize the utility of Atlassian data and operations.
## 1. Introduction to AI Integration with MCP
*This section will be expanded in a future update.*
## 2. Prompt Engineering for MCP Resources
*This section will be expanded in a future update.*
### 2.1. Resource Discovery Prompts
*This section will be expanded in a future update.*
### 2.2. Effective Query Formulation
*This section will be expanded in a future update.*
### 2.3. Parameter Selection Strategies
*This section will be expanded in a future update.*
## 3. Tool Invocation Patterns
*This section will be expanded in a future update.*
### 3.1. Identifying Tool Opportunities
*This section will be expanded in a future update.*
### 3.2. Parameter Preparation
*This section will be expanded in a future update.*
### 3.3. Multi-step Tool Sequences
*This section will be expanded in a future update.*
## 4. Data Sampling Techniques
*This section will be expanded in a future update.*
### 4.1. Representative Sampling
*This section will be expanded in a future update.*
### 4.2. Handling Large Datasets
*This section will be expanded in a future update.*
### 4.3. Context Window Optimization
*This section will be expanded in a future update.*
## 5. Response Processing
*This section will be expanded in a future update.*
### 5.1. Extracting Structured Data
*This section will be expanded in a future update.*
### 5.2. ADF Content Handling
*This section will be expanded in a future update.*
### 5.3. Error Interpretation
*This section will be expanded in a future update.*
## 6. Advanced Integration Techniques
*This section will be expanded in a future update.*
### 6.1. Multi-resource Correlation
*This section will be expanded in a future update.*
### 6.2. Workflow Automation
*This section will be expanded in a future update.*
### 6.3. Decision Support Systems
*This section will be expanded in a future update.*
## 7. Performance Optimization
*This section will be expanded in a future update.*
### 7.1. Reducing Token Usage
*This section will be expanded in a future update.*
### 7.2. Caching Strategies
*This section will be expanded in a future update.*
### 7.3. Request Batching
*This section will be expanded in a future update.*
## 8. Case Studies
*This section will be expanded in a future update.*
### 8.1. Project Management Assistant
*This section will be expanded in a future update.*
### 8.2. Documentation Generator
*This section will be expanded in a future update.*
### 8.3. Issue Analyzer
*This section will be expanded in a future update.*
## 9. References
- [MCP Protocol Documentation](https://github.com/modelcontextprotocol/mcp)
- [Prompt Engineering Guide](https://www.promptingguide.ai/)
- [MCP Server Architecture Overview](01-mcp-overview-architecture.md)
- [MCP Tools and Resources Guide](02-mcp-tools-resources.md)
---
*This document is a placeholder and will be expanded in a future update.*
```
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
```markdown
# MCP Atlassian Server 2.1.1
🚀 **Major refactor: Standardized resource/tool structure, removed content-metadata resource, updated developer documentation!**
Available on npm (@phuc-nt/mcp-atlassian-server) or download directly. Use with Cline or any MCP-compatible client.
---
### Updates in 2.1.1
**Refactor & Standardization**
- Refactored the entire codebase to standardize resource/tool structure, completely removed the content-metadata resource, and merged metadata into the page resource.
- Updated and standardized developer documentation, making it easy for any developer to extend and maintain.
- Ensured compatibility with the latest MCP SDK, improved security, scalability, and maintainability.
- Updated `docs/introduction/resources-and-tools.md` to remove all references to content-metadata.
**Bug Fixes**
- Fixed duplicate resource registration issues for a more stable experience
- Improved resource management and registration process
- Resolved issues with conflicting resource patterns
**Documentation Series**
- Added comprehensive documentation series:
1. MCP Overview & Architecture: Core concepts and design principles
2. MCP Tools & Resources Development: How to develop and extend resources/tools
3. MCP Prompts & Sampling: Guide for prompt engineering with MCP
- Updated installation guide and client development documentation
- Enhanced resource and tool descriptions
**Core Features**
**Jira Information Access**
- View issues, projects, users, comments, transitions, assignable users
- Access boards, sprints, filters, dashboards and gadgets
- Search issues with powerful filter tools
**Jira Actions**
- Create, update, transition, assign issues
- Manage boards and sprints for Agile/Scrum workflows
- Create/update dashboards, add/remove gadgets
- Create, update, and delete filters
**Confluence Information Access**
- View spaces, pages, child pages, details, comments, labels
- Access page versions and attachments
- View and search comments
**Confluence Actions**
- Create and update pages, add/remove labels, add comments
- Manage page versions, upload/download attachments
- Update and delete comments
- Delete pages
---
**How to use:**
1. Install from npm: `npm install -g @phuc-nt/mcp-atlassian-server`
2. Point Cline config to the installed package.
3. Set your Atlassian API credentials.
4. Start using natural language to work with Jira & Confluence!
See [README.md](https://github.com/phuc-nt/mcp-atlassian-server) and the new documentation series for full instructions.
Feedback and contributions are welcome! 🚀
## What's Changed
* Fixed resource registration to prevent duplicates
* Improved server stability and resource management
* Added comprehensive documentation series in `docs/knowledge/`
* Enhanced development guide for client integrations
* Updated resource structure for better organization
**Previous Changelog (2.0.0)**:
* Updated to latest Atlassian APIs (Jira API v3, Confluence API v2)
* Redesigned resource and tool structure for better organization
* Expanded Jira capabilities with board, sprint, dashboard, and filter management
* Enhanced Confluence features with advanced page operations and comment management
**Full Changelog**: https://github.com/phuc-nt/mcp-atlassian-server/blob/main/CHANGELOG.md
```
--------------------------------------------------------------------------------
/src/tools/confluence/delete-footer-comment.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { deleteConfluenceFooterCommentV2 } from '../../utils/confluence-tool-api.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('ConfluenceTools:deleteFooterComment');
export const deleteFooterCommentSchema = z.object({
commentId: z.union([z.string(), z.number()]).describe('ID of the comment to delete (required)')
});
type DeleteFooterCommentParams = z.infer<typeof deleteFooterCommentSchema>;
export async function deleteFooterCommentHandler(
params: DeleteFooterCommentParams,
config: AtlassianConfig
): Promise<{ success: boolean; id: string|number; message: string }> {
try {
logger.info(`Deleting footer comment (v2) with ID: ${params.commentId}`);
await deleteConfluenceFooterCommentV2(config, params.commentId);
return {
success: true,
id: params.commentId,
message: `Footer comment ${params.commentId} đã được xóa vĩnh viễn.`
};
} catch (error) {
if (error instanceof ApiError) throw error;
logger.error(`Error deleting footer comment (v2) with ID ${params.commentId}:`, error);
throw new ApiError(ApiErrorType.SERVER_ERROR, `Failed to delete footer comment: ${error instanceof Error ? error.message : String(error)}`, 500);
}
}
export const registerDeleteFooterCommentTool = (server: McpServer) => {
server.tool(
'deleteFooterComment',
'Delete a footer comment in Confluence (API v2)',
deleteFooterCommentSchema.shape,
async (params: DeleteFooterCommentParams, context: Record<string, any>) => {
try {
const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
if (!config) {
return {
content: [
{ type: 'text', text: 'Invalid or missing Atlassian configuration' }
],
isError: true
};
}
const result = await deleteFooterCommentHandler(params, config);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
id: result.id
})
}
]
};
} catch (error) {
if (error instanceof ApiError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: error.message,
code: error.code,
statusCode: error.statusCode,
type: error.type
})
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Error while deleting footer comment: ${error instanceof Error ? error.message : String(error)}`
})
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/confluence/delete-page.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { deleteConfluencePageV2 } from '../../utils/confluence-tool-api.js';
import { Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('ConfluenceTools:deletePage');
export const deletePageSchema = z.object({
pageId: z.string().describe('ID of the page to delete (required)'),
draft: z.boolean().optional().describe('Delete draft version if true'),
purge: z.boolean().optional().describe('Permanently delete (purge) if true')
});
type DeletePageParams = z.infer<typeof deletePageSchema>;
// Main handler
export async function deletePageHandler(
params: DeletePageParams,
config: AtlassianConfig
): Promise<{ success: boolean; message: string }> {
try {
logger.info(`Deleting page (v2) with ID: ${params.pageId}`);
await deleteConfluencePageV2(config, params);
return { success: true, message: `Page ${params.pageId} deleted successfully.` };
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
logger.error(`Error deleting page (v2) with ID ${params.pageId}:`, error);
let message = `Failed to delete page: ${error instanceof Error ? error.message : String(error)}`;
throw new ApiError(
ApiErrorType.SERVER_ERROR,
message,
500
);
}
}
// Register tool
export const registerDeletePageTool = (server: McpServer) => {
server.tool(
'deletePage',
'Delete a Confluence page (API v2)',
deletePageSchema.shape,
async (params: DeletePageParams, context: Record<string, any>) => {
try {
const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
if (!config) {
return {
content: [
{ type: 'text', text: 'Invalid or missing Atlassian configuration' }
],
isError: true
};
}
const result = await deletePageHandler(params, config);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message
})
}
]
};
} catch (error) {
if (error instanceof ApiError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: error.message,
code: error.code,
statusCode: error.statusCode,
type: error.type
})
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Error while deleting page: ${error instanceof Error ? error.message : String(error)}`
})
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/jira/add-gadget-to-dashboard.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { addGadgetToDashboard } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { Tools, Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraTools:addGadgetToDashboard');
const colorEnum = z.enum(['blue', 'red', 'yellow', 'green', 'cyan', 'purple', 'gray', 'white']);
const addGadgetToDashboardBaseSchema = z.object({
dashboardId: z.string().describe('Dashboard ID'),
moduleKey: z.string().optional().describe('Gadget moduleKey (recommended, e.g. "com.atlassian.plugins.atlassian-connect-plugin:sample-dashboard-item"). Only one of moduleKey or uri should be provided.'),
uri: z.string().optional().describe('Gadget URI (legacy, e.g. "/rest/gadgets/1.0/g/com.atlassian.jira.gadgets:filter-results-gadget/gadgets/filter-results-gadget.xml"). Only one of moduleKey or uri should be provided.'),
title: z.string().optional().describe('Gadget title (optional)'),
color: colorEnum.describe('Gadget color. Must be one of: blue, red, yellow, green, cyan, purple, gray, white.'),
position: z.object({
column: z.number().describe('Column index (0-based)'),
row: z.number().describe('Row index (0-based)')
}).optional().describe('Position of the gadget on the dashboard (optional)')
});
export const addGadgetToDashboardSchema = addGadgetToDashboardBaseSchema.refine(
(data) => !!data.moduleKey !== !!data.uri,
{ message: 'You must provide either moduleKey or uri, but not both.' }
);
type AddGadgetToDashboardParams = z.infer<typeof addGadgetToDashboardBaseSchema>;
async function addGadgetToDashboardToolImpl(params: AddGadgetToDashboardParams, context: any) {
if (!!params.moduleKey === !!params.uri) {
return {
success: false,
error: 'You must provide either moduleKey or uri, but not both.'
};
}
const config = Config.getConfigFromContextOrEnv(context);
const { dashboardId, moduleKey, uri, ...rest } = params;
let gadgetUri = uri;
if (!gadgetUri && moduleKey) {
return {
success: false,
error: 'Jira Cloud API chỉ hỗ trợ thêm gadget qua uri. Vui lòng cung cấp uri hợp lệ.'
};
}
if (!gadgetUri) {
return {
success: false,
error: 'Thiếu uri gadget.'
};
}
const data = { uri: gadgetUri, ...rest };
const result = await addGadgetToDashboard(config, dashboardId, data);
return {
success: true,
dashboardId,
uri: gadgetUri,
...rest,
result
};
}
export const registerAddGadgetToDashboardTool = (server: McpServer) => {
server.tool(
'addGadgetToDashboard',
'Add gadget to Jira dashboard (POST /rest/api/3/dashboard/{dashboardId}/gadget)',
addGadgetToDashboardBaseSchema.shape,
async (params: AddGadgetToDashboardParams, context: Record<string, any>) => {
try {
const result = await addGadgetToDashboardToolImpl(params, context);
return {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
};
} catch (error) {
logger.error('Error in addGadgetToDashboard:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) })
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/test-jira-projects.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from 'path';
import { fileURLToPath } from "url";
import fs from "fs";
// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load environment variables from .env
function loadEnv(): Record<string, string> {
try {
const envFile = path.resolve(process.cwd(), '.env');
const envContent = fs.readFileSync(envFile, 'utf8');
const envVars: Record<string, string> = {};
envContent.split('\n').forEach(line => {
if (line.trim().startsWith('#') || !line.trim()) return;
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=');
envVars[key.trim()] = value.trim();
}
});
return envVars;
} catch (error) {
console.error("Error loading .env file:", error);
return {};
}
}
// Print metadata and schema
function printResourceMetaAndSchema(res: any) {
if (res.contents && res.contents.length > 0) {
const content = res.contents[0];
// Print metadata if exists
if (content.metadata) {
console.log("Metadata:", content.metadata);
}
// Print schema if exists
if (content.schema) {
console.log("Schema:", JSON.stringify(content.schema, null, 2));
}
// Try to parse text if exists
if (content.text) {
try {
const data = JSON.parse(String(content.text));
if (Array.isArray(data)) {
console.log("Data (array, first element):", data[0]);
} else if (typeof data === 'object') {
console.log("Data (object):", data);
} else {
console.log("Data:", data);
}
} catch {
console.log("Cannot parse text.");
}
}
}
}
async function main() {
const client = new Client({
name: "mcp-atlassian-test-client-jira-projects",
version: "1.0.0"
});
// Path to MCP server
const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
// Load environment variables
const envVars = loadEnv();
const processEnv: Record<string, string> = {};
Object.keys(process.env).forEach(key => {
if (process.env[key] !== undefined) {
processEnv[key] = process.env[key] as string;
}
});
// Initialize transport
const transport = new StdioClientTransport({
command: "node",
args: [serverPath],
env: {
...processEnv,
...envVars
}
});
// Connect to server
console.log("Connecting to MCP server...");
await client.connect(transport);
console.log("\n=== Test Jira Projects Resource ===");
// Change projectKey to match your environment if needed
const projectKey = "XDEMO2";
const resourceUris = [
`jira://projects`,
`jira://projects/${projectKey}`,
`jira://projects/${projectKey}/roles`
];
for (const uri of resourceUris) {
try {
console.log(`\nResource: ${uri}`);
const res = await client.readResource({ uri });
if (uri === "jira://projects") {
const projectsData = JSON.parse(String(res.contents[0].text));
console.log("Number of projects:", projectsData.projects?.length || 0);
}
printResourceMetaAndSchema(res);
} catch (e) {
console.error(`Resource ${uri} error:`, e instanceof Error ? e.message : e);
}
}
console.log("\n=== Finished testing Jira Projects Resource! ===");
await client.close();
}
main();
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/test-jira-issues.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from 'path';
import { fileURLToPath } from "url";
import fs from "fs";
// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load environment variables from .env
function loadEnv(): Record<string, string> {
try {
const envFile = path.resolve(process.cwd(), '.env');
const envContent = fs.readFileSync(envFile, 'utf8');
const envVars: Record<string, string> = {};
envContent.split('\n').forEach(line => {
if (line.trim().startsWith('#') || !line.trim()) return;
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=');
envVars[key.trim()] = value.trim();
}
});
return envVars;
} catch (error) {
console.error("Error loading .env file:", error);
return {};
}
}
// Print metadata and schema
function printResourceMetaAndSchema(res: any) {
if (res.contents && res.contents.length > 0) {
const content = res.contents[0];
// Print metadata if exists
if (content.metadata) {
console.log("Metadata:", content.metadata);
}
// Print schema if exists
if (content.schema) {
console.log("Schema:", JSON.stringify(content.schema, null, 2));
}
// Try to parse text if exists
if (content.text) {
try {
const data = JSON.parse(String(content.text));
if (Array.isArray(data)) {
console.log("Data (array, first element):", data[0]);
} else if (typeof data === 'object') {
console.log("Data (object):", data);
} else {
console.log("Data:", data);
}
} catch {
console.log("Cannot parse text.");
}
}
}
}
async function main() {
const client = new Client({
name: "mcp-atlassian-test-client-jira-issues",
version: "1.0.0"
});
// Path to MCP server
const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
// Load environment variables
const envVars = loadEnv();
const processEnv: Record<string, string> = {};
Object.keys(process.env).forEach(key => {
if (process.env[key] !== undefined) {
processEnv[key] = process.env[key] as string;
}
});
// Initialize transport
const transport = new StdioClientTransport({
command: "node",
args: [serverPath],
env: {
...processEnv,
...envVars
}
});
// Connect to server
console.log("Connecting to MCP server...");
await client.connect(transport);
console.log("\n=== Test Jira Issues Resource ===");
// Change issueKey to match your environment if needed
const issueKey = "XDEMO2-53";
const resourceUris = [
`jira://issues`,
`jira://issues/${issueKey}`,
`jira://issues/${issueKey}/transitions`,
`jira://issues/${issueKey}/comments`
];
for (const uri of resourceUris) {
try {
console.log(`\nResource: ${uri}`);
const res = await client.readResource({ uri });
if (uri === "jira://issues") {
const issuesData = JSON.parse(String(res.contents[0].text));
console.log("Number of issues:", issuesData.issues?.length || 0);
}
printResourceMetaAndSchema(res);
} catch (e) {
console.error(`Resource ${uri} error:`, e instanceof Error ? e.message : e);
}
}
console.log("\n=== Finished testing Jira Issues Resource! ===");
await client.close();
}
main();
```
--------------------------------------------------------------------------------
/src/tools/confluence/update-page-title.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { updateConfluencePageTitleV2 } from '../../utils/confluence-tool-api.js';
import { Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('ConfluenceTools:updatePageTitle');
export const updatePageTitleSchema = z.object({
pageId: z.string().describe('ID of the page to update the title (required)'),
title: z.string().describe('New title of the page (required)'),
version: z.number().describe('New version number (required, must be exactly one greater than the current version)')
});
type UpdatePageTitleParams = z.infer<typeof updatePageTitleSchema>;
export async function updatePageTitleHandler(
params: UpdatePageTitleParams,
config: AtlassianConfig
): Promise<{ success: boolean; id: string; title: string; version: number; message: string }> {
try {
logger.info(`Updating page title (v2) with ID: ${params.pageId}`);
const data = await updateConfluencePageTitleV2(config, params);
return {
success: true,
id: data.id,
title: data.title,
version: data.version?.number,
message: `Page ${data.id} title updated to "${data.title}" (version ${data.version?.number}) successfully.`
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
logger.error(`Error updating page title (v2) with ID ${params.pageId}:`, error);
let message = `Failed to update page title: ${error instanceof Error ? error.message : String(error)}`;
throw new ApiError(
ApiErrorType.SERVER_ERROR,
message,
500
);
}
}
export const registerUpdatePageTitleTool = (server: McpServer) => {
server.tool(
'updatePageTitle',
'Update the title of a Confluence page (API v2)',
updatePageTitleSchema.shape,
async (params: UpdatePageTitleParams, context: Record<string, any>) => {
try {
const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
if (!config) {
return {
content: [
{ type: 'text', text: 'Invalid or missing Atlassian configuration' }
],
isError: true
};
}
const result = await updatePageTitleHandler(params, config);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
id: result.id,
title: result.title,
version: result.version
})
}
]
};
} catch (error) {
if (error instanceof ApiError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: error.message,
code: error.code,
statusCode: error.statusCode,
type: error.type
})
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Error while updating page title: ${error instanceof Error ? error.message : String(error)}`
})
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/tools/confluence/update-footer-comment.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { updateConfluenceFooterCommentV2 } from '../../utils/confluence-tool-api.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Config } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('ConfluenceTools:updateFooterComment');
export const updateFooterCommentSchema = z.object({
commentId: z.union([z.string(), z.number()]).describe('ID of the comment to update (required)'),
version: z.number().describe('New version number (required, must be exactly one greater than the current version)'),
value: z.string().describe('New content of the comment (required)'),
representation: z.string().optional().describe('Content representation, default is "storage"'),
message: z.string().optional().describe('Update message (optional)')
});
type UpdateFooterCommentParams = z.infer<typeof updateFooterCommentSchema>;
export async function updateFooterCommentHandler(
params: UpdateFooterCommentParams,
config: AtlassianConfig
): Promise<{ success: boolean; id: string|number; version: number; message: string }> {
try {
logger.info(`Updating footer comment (v2) with ID: ${params.commentId}`);
const data = await updateConfluenceFooterCommentV2(config, params);
return {
success: true,
id: params.commentId,
version: data.version?.number,
message: `Footer comment ${params.commentId} updated to version ${data.version?.number} thành công.`
};
} catch (error) {
if (error instanceof ApiError) throw error;
logger.error(`Error updating footer comment (v2) with ID ${params.commentId}:`, error);
throw new ApiError(ApiErrorType.SERVER_ERROR, `Failed to update footer comment: ${error instanceof Error ? error.message : String(error)}`, 500);
}
}
export const registerUpdateFooterCommentTool = (server: McpServer) => {
server.tool(
'updateFooterComment',
'Update a footer comment in Confluence (API v2)',
updateFooterCommentSchema.shape,
async (params: UpdateFooterCommentParams, context: Record<string, any>) => {
try {
const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
if (!config) {
return {
content: [
{ type: 'text', text: 'Invalid or missing Atlassian configuration' }
],
isError: true
};
}
const result = await updateFooterCommentHandler(params, config);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
id: result.id,
version: result.version
})
}
]
};
} catch (error) {
if (error instanceof ApiError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: error.message,
code: error.code,
statusCode: error.statusCode,
type: error.type
})
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Error while updating footer comment: ${error instanceof Error ? error.message : String(error)}`
})
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/test-confluence-spaces.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from 'path';
import { fileURLToPath } from "url";
import fs from "fs";
// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load environment variables from .env
function loadEnv(): Record<string, string> {
try {
const envFile = path.resolve(process.cwd(), '.env');
const envContent = fs.readFileSync(envFile, 'utf8');
const envVars: Record<string, string> = {};
envContent.split('\n').forEach(line => {
if (line.trim().startsWith('#') || !line.trim()) return;
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=');
envVars[key.trim()] = value.trim();
}
});
return envVars;
} catch (error) {
console.error("Error loading .env file:", error);
return {};
}
}
// Print metadata and schema
function printResourceMetaAndSchema(res: any) {
if (res.contents && res.contents.length > 0) {
const content = res.contents[0];
// Print metadata if exists
if (content.metadata) {
console.log("Metadata:", content.metadata);
}
// Print schema if exists
if (content.schema) {
console.log("Schema:", JSON.stringify(content.schema, null, 2));
}
// Try to parse text if exists
if (content.text) {
try {
const data = JSON.parse(String(content.text));
if (Array.isArray(data)) {
console.log("Data (array, first element):", data[0]);
} else if (typeof data === 'object') {
console.log("Data (object):", data);
} else {
console.log("Data:", data);
}
} catch {
console.log("Cannot parse text.");
}
}
}
}
async function main() {
const client = new Client({
name: "mcp-atlassian-test-client-confluence-spaces",
version: "1.0.0"
});
// Path to MCP server
const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
// Load environment variables
const envVars = loadEnv();
const processEnv: Record<string, string> = {};
Object.keys(process.env).forEach(key => {
if (process.env[key] !== undefined) {
processEnv[key] = process.env[key] as string;
}
});
// Initialize transport
const transport = new StdioClientTransport({
command: "node",
args: [serverPath],
env: {
...processEnv,
...envVars
}
});
// Connect to server
console.log("Connecting to MCP server...");
await client.connect(transport);
console.log("\n=== Test Confluence Spaces Resource ===");
// Nếu có biến spaceKey hoặc pageId, hãy cập nhật:
const spaceKey = "AWA1"; // Space key mới
const homePageId = "19464453"; // Home page id mới
const resourceUris = [
`confluence://spaces`,
`confluence://spaces/${spaceKey}`,
`confluence://spaces/${spaceKey}/pages`
];
for (const uri of resourceUris) {
try {
console.log(`\nResource: ${uri}`);
const res = await client.readResource({ uri });
if (uri === "confluence://spaces") {
const spacesData = JSON.parse(String(res.contents[0].text));
console.log("Number of spaces:", spacesData.spaces?.length || 0);
} else if (uri.includes("/pages")) {
const pagesData = JSON.parse(String(res.contents[0].text));
console.log("Number of pages:", pagesData.pages?.length || 0);
}
printResourceMetaAndSchema(res);
} catch (e) {
console.error(`Resource ${uri} error:`, e instanceof Error ? e.message : e);
}
}
console.log("\n=== Finished testing Confluence Spaces Resource! ===");
await client.close();
}
main();
```
--------------------------------------------------------------------------------
/src/tools/confluence/add-comment.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { callConfluenceApi } from '../../utils/atlassian-api-base.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { addConfluenceCommentV2 } from '../../utils/confluence-tool-api.js';
import { Config, Tools } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('ConfluenceTools:addComment');
// Input parameter schema
export const addCommentSchema = z.object({
pageId: z.string().describe('ID of the page to add a comment to'),
content: z.string().describe('Content of the comment (Confluence storage format, XML-like HTML)')
});
type AddCommentParams = z.infer<typeof addCommentSchema>;
interface AddCommentResult {
id: string;
created: string;
author: string;
body: string;
success: boolean;
}
// Main handler to add a comment to a page (API v2)
export async function addCommentHandler(
params: AddCommentParams,
config: AtlassianConfig
): Promise<AddCommentResult> {
try {
logger.info(`Adding comment (v2) to page: ${params.pageId}`);
const data = await addConfluenceCommentV2(config, {
pageId: params.pageId,
content: params.content
});
return {
id: data.id,
created: data.createdAt,
author: data.createdBy?.displayName || '',
body: data.body?.value || '',
success: true
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
logger.error(`Error adding comment (v2) to page ${params.pageId}:`, error);
let message = `Failed to add comment: ${error instanceof Error ? error.message : String(error)}`;
throw new ApiError(
ApiErrorType.SERVER_ERROR,
message,
500
);
}
}
// Register the tool with MCP Server
export const registerAddCommentTool = (server: McpServer) => {
server.tool(
'addComment',
'Add a comment to a Confluence page',
addCommentSchema.shape,
async (params: AddCommentParams, context: Record<string, any>) => {
try {
const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
if (!config) {
return {
content: [
{ type: 'text', text: 'Invalid or missing Atlassian configuration' }
],
isError: true
};
}
const result = await addCommentHandler(params, config);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Comment added successfully with ID: ${result.id}`,
id: result.id,
created: result.created,
author: result.author,
body: result.body
})
}
]
};
} catch (error) {
if (error instanceof ApiError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: error.message,
code: error.code,
statusCode: error.statusCode,
type: error.type
})
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Error while adding comment: ${error instanceof Error ? error.message : String(error)}`
})
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/src/utils/confluence-tool-api.ts:
--------------------------------------------------------------------------------
```typescript
import { AtlassianConfig } from './atlassian-api-base.js';
import { callConfluenceApi } from './atlassian-api-base.js';
// Create a new Confluence page (API v2)
export async function createConfluencePageV2(config: AtlassianConfig, params: { spaceId: string, title: string, content: string, parentId?: string }): Promise<any> {
// Validate content
if (!params.content.trim().startsWith('<')) {
throw new Error('Content must be in Confluence storage format (XML-like HTML).');
}
// Chuẩn bị payload
const requestData: any = {
spaceId: params.spaceId,
title: params.title,
body: {
representation: 'storage',
value: params.content
}
};
if (params.parentId) requestData.parentId = params.parentId;
// Gọi API tạo page
return await callConfluenceApi<any>(
config,
`/api/v2/pages`,
'POST',
requestData
);
}
// Update a Confluence page (API v2)
// Có thể update title hoặc body hoặc cả hai. Mỗi lần update phải tăng version.
export async function updateConfluencePageV2(config: AtlassianConfig, params: { pageId: string, title: string, content: string, version: number }): Promise<any> {
const payload = {
id: params.pageId,
status: "current",
title: params.title,
body: {
representation: "storage",
value: params.content
},
version: {
number: params.version
}
};
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(params.pageId)}`,
'PUT',
payload
);
}
// Add a comment to a Confluence page (API v2)
export async function addConfluenceCommentV2(config: AtlassianConfig, params: { pageId: string, content: string }): Promise<any> {
if (!params.content.trim().startsWith('<')) {
throw new Error('Comment content must be in Confluence storage format (XML-like HTML).');
}
const requestData = {
pageId: params.pageId,
body: {
representation: 'storage',
value: params.content
}
};
return await callConfluenceApi<any>(
config,
`/api/v2/footer-comments`,
'POST',
requestData
);
}
// Delete a Confluence page (API v2)
export async function deleteConfluencePageV2(config: AtlassianConfig, params: { pageId: string, draft?: boolean, purge?: boolean }): Promise<any> {
const query: string[] = [];
if (params.draft) query.push('draft=true');
if (params.purge) query.push('purge=true');
const endpoint = `/api/v2/pages/${encodeURIComponent(params.pageId)}` + (query.length ? `?${query.join('&')}` : '');
return await callConfluenceApi<any>(
config,
endpoint,
'DELETE'
);
}
// Update Confluence page title (API v2)
export async function updateConfluencePageTitleV2(config: AtlassianConfig, params: { pageId: string, title: string, version: number }): Promise<any> {
const payload = {
title: params.title,
version: { number: params.version },
status: "current"
};
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(params.pageId)}/title`,
'PUT',
payload
);
}
// Update a footer comment in Confluence (API v2)
export async function updateConfluenceFooterCommentV2(config: AtlassianConfig, params: { commentId: string|number, version: number, value: string, representation?: string, message?: string }): Promise<any> {
const payload: any = {
version: {
number: params.version
},
body: {
representation: params.representation || 'storage',
value: params.value
}
};
if (params.message) payload.version.message = params.message;
return await callConfluenceApi<any>(
config,
`/api/v2/footer-comments/${encodeURIComponent(params.commentId)}`,
'PUT',
payload
);
}
// Delete a footer comment in Confluence (API v2)
export async function deleteConfluenceFooterCommentV2(config: AtlassianConfig, commentId: string|number): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/footer-comments/${encodeURIComponent(commentId)}`,
'DELETE'
);
}
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/test-jira-users.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from 'path';
import { fileURLToPath } from "url";
import fs from "fs";
// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load environment variables from .env
function loadEnv(): Record<string, string> {
try {
const envFile = path.resolve(process.cwd(), '.env');
const envContent = fs.readFileSync(envFile, 'utf8');
const envVars: Record<string, string> = {};
envContent.split('\n').forEach(line => {
if (line.trim().startsWith('#') || !line.trim()) return;
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=');
envVars[key.trim()] = value.trim();
}
});
return envVars;
} catch (error) {
console.error("Error loading .env file:", error);
return {};
}
}
// Print metadata and schema
function printResourceMetaAndSchema(res: any) {
if (res.contents && res.contents.length > 0) {
const content = res.contents[0];
// Print metadata if exists
if (content.metadata) {
console.log("Metadata:", content.metadata);
}
// Print schema if exists
if (content.schema) {
console.log("Schema:", JSON.stringify(content.schema, null, 2));
}
// Try to parse text if exists
if (content.text) {
try {
const data = JSON.parse(String(content.text));
if (Array.isArray(data)) {
console.log("Data (array, first element):", data[0]);
} else if (typeof data === 'object') {
console.log("Data (object):", data);
} else {
console.log("Data:", data);
}
} catch {
console.log("Cannot parse text.");
}
}
}
}
async function main() {
const client = new Client({
name: "mcp-atlassian-test-client-jira-users",
version: "1.0.0"
});
// Path to MCP server
const serverPath = "/opt/homebrew/lib/node_modules/@phuc-nt/mcp-atlassian-server/dist/index.js";
// Load environment variables
const envVars = loadEnv();
const processEnv: Record<string, string> = {};
Object.keys(process.env).forEach(key => {
if (process.env[key] !== undefined) {
processEnv[key] = process.env[key] as string;
}
});
// Initialize transport
const transport = new StdioClientTransport({
command: "node",
args: [serverPath],
env: {
...processEnv,
...envVars
}
});
// Connect to server
console.log("Connecting to MCP server...");
await client.connect(transport);
console.log("\n=== Test Jira Users Resource ===");
// Change these values to match your environment if needed
const accountId = "557058:24acce7b-a0c1-4f45-97f1-7eb4afd2ff5f";
const projectKey = "XDEMO2";
const roleId = "10002"; // Example roleId, get the correct one from project roles
// NOTE: Resource jira://users has been removed because it requires query parameters
// (username or accountId) and cannot be accessed directly without parameters.
// We only test more specific resources below.
const resourceUris = [
`jira://users/${accountId}`,
`jira://users/assignable/${projectKey}`,
`jira://users/role/${projectKey}/${roleId}`
];
for (const uri of resourceUris) {
try {
console.log(`\nResource: ${uri}`);
const res = await client.readResource({ uri });
if (uri.startsWith("jira://users/")) {
const userData = JSON.parse(String(res.contents[0].text));
if (userData.user) {
console.log("User:", {
accountId: userData.user.accountId,
displayName: userData.user.displayName,
emailAddress: userData.user.emailAddress,
active: userData.user.active
});
} else if (userData.users && userData.users.length > 0) {
console.log("Number of users:", userData.users.length);
}
}
printResourceMetaAndSchema(res);
} catch (e) {
console.error(`Resource ${uri} error:`, e instanceof Error ? e.message : e);
}
}
console.log("\n=== Finished testing Jira Users Resource! ===");
await client.close();
}
main();
```
--------------------------------------------------------------------------------
/docs/dev-guide/advance-resource-tool.md:
--------------------------------------------------------------------------------
```markdown
# Hướng Dẫn Implementation: Bổ Sung Resources và Tools cho MCP Atlassian Server (Chuẩn hóa atlassian-api.ts)
Dưới đây là hướng dẫn chi tiết để bổ sung resource/tool mới cho MCP Atlassian Server, **chỉ sử dụng các hàm từ `atlassian-api.ts`** (KHÔNG sử dụng `jiraClient`, `confluenceClient` hay `atlassian-client.js`).
---
## I. Bổ Sung Resources
### 1. Jira: Filters
```typescript
import { getFilters, getFilterById, getMyFilters } from '../../utils/atlassian-api.js';
// Danh sách filter
const response = await getFilters(config, offset, limit);
// Chi tiết filter
const filter = await getFilterById(config, filterId);
// Filter cá nhân
const myFilters = await getMyFilters(config);
```
### 2. Jira: Boards
```typescript
import { getBoards, getBoardById, getBoardIssues } from '../../utils/atlassian-api.js';
// Danh sách boards
const response = await getBoards(config, offset, limit);
// Chi tiết board
const board = await getBoardById(config, boardId);
// Issues trong board
const issues = await getBoardIssues(config, boardId, offset, limit);
```
### 3. Jira: Sprints
```typescript
import { getSprintsByBoard, getSprintById, getSprintIssues } from '../../utils/atlassian-api.js';
// Danh sách sprints trong board
const response = await getSprintsByBoard(config, boardId, offset, limit);
// Chi tiết sprint
const sprint = await getSprintById(config, sprintId);
// Issues trong sprint
const issues = await getSprintIssues(config, sprintId, offset, limit);
```
### 4. Confluence: Labels, Attachments, Content Versions
```typescript
import { getPageLabels, getPageAttachments, getPageVersions } from '../../utils/atlassian-api.js';
// Labels của page
const response = await getPageLabels(config, pageId, offset, limit);
// Attachments của page
const response = await getPageAttachments(config, pageId, offset, limit);
// Versions của page
const response = await getPageVersions(config, pageId, offset, limit);
```
---
## II. Bổ Sung Tools
### 1. Filter Tools
```typescript
import { createFilter, updateFilter, deleteFilter } from '../../utils/atlassian-api.js';
// Tạo filter
const response = await createFilter(config, name, jql, description, favourite);
// Cập nhật filter
const response = await updateFilter(config, filterId, { name, jql, description, favourite });
// Xóa filter
await deleteFilter(config, filterId);
```
### 2. Sprint Tools
```typescript
import { createSprint } from '../../utils/atlassian-api.js';
// Tạo sprint
const response = await createSprint(config, boardId, name, startDate, endDate, goal);
```
### 3. Confluence Label Tools
```typescript
import { addLabelsToPage, removeLabelsFromPage } from '../../utils/atlassian-api.js';
// Thêm label vào page
await addLabelsToPage(config, pageId, labels);
// Xóa label khỏi page
await removeLabelsFromPage(config, pageId, labels);
```
---
## III. Lưu ý Quan Trọng
1. **Chỉ sử dụng các hàm từ `atlassian-api.ts`** để thao tác với Jira/Confluence. KHÔNG sử dụng `jiraClient`, `confluenceClient`, hoặc bất kỳ client wrapper JS nào khác.
2. **API Endpoints**: Jira Agile API sử dụng `/rest/agile/1.0/`, Jira core sử dụng `/rest/api/3/`, Confluence sử dụng `/wiki/rest/api/` (các hàm trong `atlassian-api.ts` đã chuẩn hóa sẵn).
3. **Xử lý ADF**: Sử dụng hàm `adfToMarkdown` trong `atlassian-api.ts` nếu cần chuyển đổi nội dung rich text.
4. **Phân trang**: Các hàm resource đều hỗ trợ `offset`, `limit`.
5. **Schema**: Đảm bảo resource/tool trả về đúng schema đã định nghĩa.
6. **Error Handling**: Sử dụng try/catch và trả về lỗi rõ ràng.
---
## IV. Ví dụ tổng quát
```typescript
// Lấy danh sách filter
const filters = await getFilters(config, 0, 20);
// Tạo filter mới
const newFilter = await createFilter(config, 'My Filter', 'project = TEST', 'Test filter', true);
// Thêm label vào page Confluence
await addLabelsToPage(config, '123456', ['important', 'urgent']);
```
---
**Tóm lại:**
- Luôn import và sử dụng các hàm từ `atlassian-api.ts`.
- Không còn bất kỳ import hoặc hướng dẫn nào liên quan đến `atlassian-client.js`, `jiraClient`, `confluenceClient`.
- Nếu cần mở rộng resource/tool mới, hãy viết hàm mới trong `atlassian-api.ts` rồi sử dụng lại ở resource/tool.
Với hướng dẫn này, bạn có thể triển khai đầy đủ các resource và tool mới cho MCP Atlassian Server, giúp người dùng tương tác hiệu quả hơn với Jira và Confluence thông qua AI.
```
--------------------------------------------------------------------------------
/src/tests/confluence/create-page.test.ts:
--------------------------------------------------------------------------------
```typescript
import { createPageHandler } from '../../tools/confluence/create-page.js';
import { callConfluenceApi } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
// Mock cho các dependencies
jest.mock('../../utils/atlassian-api.js');
jest.mock('../../utils/logger.js', () => ({
Logger: {
getLogger: jest.fn().mockReturnValue({
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
warn: jest.fn()
})
}
}));
describe('createPageHandler', () => {
// Thiết lập mock và cấu hình trước mỗi test
beforeEach(() => {
jest.clearAllMocks();
});
const mockConfig = {
baseUrl: 'https://test.atlassian.net',
apiToken: 'test-token',
email: '[email protected]'
};
const mockPageResponse = {
id: '12345',
type: 'page',
status: 'current',
title: 'Test Page Title',
space: {
key: 'TESTSPACE',
name: 'Test Space'
},
_links: {
webui: '/spaces/TESTSPACE/pages/12345',
self: '/rest/api/content/12345'
}
};
test('should create page successfully', async () => {
// Arrange
const mockParams = {
spaceId: 'TEST',
title: 'Test Page Title',
content: '<p>This is test content</p>'
};
(callConfluenceApi as jest.Mock).mockResolvedValue(mockPageResponse);
// Act
const result = await createPageHandler(mockParams, mockConfig);
// Assert
expect(callConfluenceApi).toHaveBeenCalledWith(
mockConfig,
'/content',
'POST',
expect.objectContaining({
type: 'page',
title: 'Test Page Title',
space: { key: 'TESTSPACE' },
body: {
storage: {
value: '<p>This is test content</p>',
representation: 'storage'
}
}
})
);
expect(result).toEqual({
id: '12345',
type: 'page',
status: 'current',
title: 'Test Page Title',
spaceKey: 'TESTSPACE',
_links: {
webui: '/spaces/TESTSPACE/pages/12345',
self: '/rest/api/content/12345'
},
success: true
});
});
test('should create page with parent ID', async () => {
// Arrange
const mockParams = {
spaceId: 'TEST',
title: 'Child Page Title',
content: '<p>This is child page content</p>',
parentId: '98765'
};
(callConfluenceApi as jest.Mock).mockResolvedValue({
...mockPageResponse,
title: 'Child Page Title'
});
// Act
const result = await createPageHandler(mockParams, mockConfig);
// Assert
expect(callConfluenceApi).toHaveBeenCalledWith(
mockConfig,
'/content',
'POST',
expect.objectContaining({
ancestors: [{ id: '98765' }]
})
);
expect(result.title).toBe('Child Page Title');
});
test('should create page with labels', async () => {
// Arrange
const mockParams = {
spaceId: 'TEST',
title: 'Labeled Page',
content: '<p>This page has labels</p>',
labels: ['test', 'documentation']
};
(callConfluenceApi as jest.Mock)
.mockResolvedValueOnce({
...mockPageResponse,
title: 'Labeled Page'
})
.mockResolvedValueOnce({}); // Response for adding labels
// Act
const result = await createPageHandler(mockParams, mockConfig);
// Assert
// Kiểm tra cuộc gọi đầu tiên để tạo trang
expect(callConfluenceApi).toHaveBeenNthCalledWith(
1,
mockConfig,
'/content',
'POST',
expect.any(Object)
);
// Kiểm tra cuộc gọi thứ hai để thêm labels
expect(callConfluenceApi).toHaveBeenNthCalledWith(
2,
mockConfig,
'/content/12345/label',
'POST',
[{ name: 'test' }, { name: 'documentation' }]
);
expect(result.title).toBe('Labeled Page');
expect(result.success).toBe(true);
});
test('should handle API error', async () => {
// Arrange
const mockParams = {
spaceId: 'INVALID',
title: 'Error Page',
content: '<p>This will cause an error</p>'
};
const mockError = new ApiError(ApiErrorType.SERVER_ERROR, 'Simulated error');
(callConfluenceApi as jest.Mock).mockRejectedValue(mockError);
// Act & Assert
await expect(createPageHandler(mockParams, mockConfig)).rejects.toThrow(mockError);
});
});
```
--------------------------------------------------------------------------------
/docs/dev-guide/mcp-client-for-testing.md:
--------------------------------------------------------------------------------
```markdown
Để tạo một MCP Client đơn giản để test các thay đổi của MCP server trước khi test với Cline, bạn có thể làm theo các bước sau:
## Tạo MCP Test Client đơn giản
### 1. Cài đặt SDK
```bash
npm install @modelcontextprotocol/sdk
```
### 2. Tạo file client.js (hoặc client.ts)
```javascript
import { Client, Implementation } from "@modelcontextprotocol/sdk/client/mcp.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";
// Đường dẫn đến MCP server của bạn
const SERVER_PATH = "./dist/index.js";
async function main() {
try {
// Khởi tạo process cho server
const process = spawn("node", [SERVER_PATH]);
// Tạo transport kết nối với server qua stdio
const transport = new StdioClientTransport({
input: process.stdout,
output: process.stdin
});
// Tạo MCP client
const client = new Client(
new Implementation("test-client", "1.0.0")
);
// Kết nối với server
await client.connect(transport);
console.log("Connected to MCP server");
// Liệt kê các resources
const resources = await client.listResources();
console.log("Available resources:", resources?.resources?.map(r => r.uri) || []);
// Liệt kê các tools
const tools = await client.listTools();
console.log("Available tools:", tools?.tools?.map(t => t.name) || []);
// Test một resource (ví dụ: jira://issues)
if (resources?.resources?.some(r => r.uri === "jira://issues")) {
const result = await client.readResource("jira://issues");
console.log("Resource result:", JSON.parse(result.contents[0].text));
}
// Test một tool (ví dụ: createIssue)
if (tools?.tools?.some(t => t.name === "createIssue")) {
const result = await client.callTool("createIssue", {
projectKey: "DEMO",
summary: "Test issue from MCP client"
});
console.log("Tool result:", result);
}
// Đóng kết nối
await client.close();
process.kill();
} catch (error) {
console.error("Error:", error);
}
}
main();
```
### 3. Tạo script test có tham số
Bạn có thể tạo một script test linh hoạt hơn, cho phép truyền tham số:
```javascript
import { Client, Implementation } from "@modelcontextprotocol/sdk/client/mcp.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";
async function testResource(client, resourceUri) {
try {
const result = await client.readResource(resourceUri);
console.log(`Resource ${resourceUri} result:`, JSON.parse(result.contents[0].text));
} catch (error) {
console.error(`Error reading resource ${resourceUri}:`, error);
}
}
async function testTool(client, toolName, params) {
try {
const result = await client.callTool(toolName, params);
console.log(`Tool ${toolName} result:`, result);
} catch (error) {
console.error(`Error calling tool ${toolName}:`, error);
}
}
async function main() {
const serverPath = process.argv[2] || "./dist/index.js";
const command = process.argv[3] || "list";
const param = process.argv[4] || "";
const jsonParams = process.argv[5] ? JSON.parse(process.argv[5]) : {};
const process = spawn("node", [serverPath]);
const transport = new StdioClientTransport({
input: process.stdout,
output: process.stdin
});
const client = new Client(new Implementation("test-client", "1.0.0"));
await client.connect(transport);
switch (command) {
case "list":
const resources = await client.listResources();
console.log("Resources:", resources?.resources?.map(r => r.uri) || []);
const tools = await client.listTools();
console.log("Tools:", tools?.tools?.map(t => t.name) || []);
break;
case "resource":
await testResource(client, param);
break;
case "tool":
await testTool(client, param, jsonParams);
break;
default:
console.log("Unknown command");
}
await client.close();
process.kill();
}
main();
```
### 4. Chạy test client
```bash
# Liệt kê tất cả resources và tools
node client.js
# Test một resource cụ thể
node client.js ./dist/index.js resource "jira://issues"
# Test một tool cụ thể với tham số
node client.js ./dist/index.js tool "createIssue" '{"projectKey":"DEMO","summary":"Test issue"}'
```
Client test này sẽ giúp bạn kiểm tra nhanh các thay đổi trong MCP server trước khi test với Cline, đặc biệt là khi bạn đang chuẩn hóa metadata và bổ sung schema cho các resource.
```
--------------------------------------------------------------------------------
/docs/test-reports/cline-installation-test-2025-05-04.md:
--------------------------------------------------------------------------------
```markdown
# MCP Atlassian Server: Installation & Functionality Test Report
## Overview
This report documents the testing of MCP Atlassian Server (by phuc-nt) when installed and used through Cline AI assistant. The test verifies the server's capabilities to interact with Atlassian Jira and Confluence, covering both Resource retrieval and Tool actions.
## Test Environment
- **Client**: Cline AI assistant
- **Installation Method**: Using llms-install.md guide
- **MCP Server**: phuc-nt/mcp-atlassian-server
- **Target Systems**: Atlassian Jira Cloud, Atlassian Confluence Cloud
- **Test Date**: According to repository history
## Installation Test
The server was successfully installed by instructing Cline to "Install MCP Atlassian Server (by phuc-nt)" following the llms-install.md guide. Cline was able to:
1. Interpret the installation instructions
2. Guide the user through configuration
3. Establish connection with the server
4. Successfully process commands through the MCP interface
## Functionality Tests
### Jira Resources & Tools
| Feature | Test Case | Result | Details |
|---------|-----------|--------|---------|
| **Resource: Issues** | Retrieve issue transitions | ✅ Success | Retrieved transitions for XDEMO2-50 |
| **Resource: Users** | List assignable users | ✅ Success | Retrieved 3 assignable users for project XDEMO2 |
| **Tool: createIssue** | Create new issue | ✅ Success | Created issue XDEMO2-50 |
| **Tool: updateIssue** | Update issue summary | ✅ Success | Changed summary to "MCP test updateIssue (auto)" |
| **Tool: transitionIssue** | Change issue status | ✅ Success | Transitioned to "In Progress" using ID 11 |
| **Tool: assignIssue** | Assign issue to user | ✅ Success | Assigned to user "LemmyC" |
#### Sample Interaction - Creating Issue:
```
Cline wants to use a tool on the `phuc-nt/mcp-atlassian-server` MCP server:
createIssue
Arguments
{
"issueIdOrKey": "XDEMO2-50",
"summary": "MCP test updateIssue (auto)"
}
Response
Issue XDEMO2-50 updated successfully
```
### Confluence Resources & Tools
| Feature | Test Case | Result | Details |
|---------|-----------|--------|---------|
| **Resource: Spaces** | List available spaces | ✅ Success | Successfully retrieved space information |
| **Tool: createPage** | Create new page | ✅ Success | Created "MCP test createPage (auto)" in space TX |
| **Tool: addComment** | Add comment to page | ✅ Success | Added comment to page with ID 14843908 |
#### Sample Interaction - Creating Page:
```
Cline wants to use a tool on the `phuc-nt/mcp-atlassian-server` MCP server:
createPage
Arguments
{
"spaceKey": "TX",
"title": "MCP test createPage (auto)",
"content": "<h1>Test Page</h1>"
}
Response
Page "MCP test createPage (auto)" created successfully in space TX.
URL: https://phuc-nt.atlassian.net/wiki/spaces/TX/pages/14843908/MCP%2Btest%2BcreatePage%2B(auto)
```
## Test Results Summary
All essential functionality of MCP Atlassian Server was successfully tested:
### Jira
- Resource queries (projects, issues, users): ✅ Successful
- Issue creation: ✅ Successful
- Issue updates: ✅ Successful
- Status transitions: ✅ Successful
- Issue assignment: ✅ Successful
### Confluence
- Resource queries (spaces): ✅ Successful
- Page creation: ✅ Successful
- Comment addition: ✅ Successful
## Notes for Users
1. **Installation Simplicity**: The server can be installed with a single command in Cline: "Install MCP Atlassian Server (by phuc-nt)", making it accessible even for users without technical expertise.
2. **Authentication Flow**: Cline will guide users through entering Atlassian credentials (site name, email, API token) during installation.
3. **Resource Usage Tips**:
- When using transitions, first query the available transitions to get their IDs
- For page operations in Confluence, note the page ID from URLs (format: .../pages/[ID]/...)
- User assignment requires retrieving the accountId first
4. **Content Formatting**:
- For Confluence pages, use simple HTML content initially
- Keep JSON requests minimal with only required fields for best results
5. **Performance**: API requests were consistently fast, with response times suitable for interactive use.
## Conclusion
MCP Atlassian Server (by phuc-nt) demonstrates reliable functionality when installed and used through Cline. The server successfully connects to both Jira and Confluence, providing a natural language interface to these tools. The integration works as expected, enabling users to perform common Atlassian tasks without leaving their AI assistant environment.
```
--------------------------------------------------------------------------------
/src/utils/confluence-resource-api.ts:
--------------------------------------------------------------------------------
```typescript
import { AtlassianConfig } from './atlassian-api-base.js';
import { callConfluenceApi } from './atlassian-api-base.js';
// Get labels of a Confluence page (API v2, cursor-based)
export async function getConfluencePageLabelsV2(config: AtlassianConfig, pageId: string, cursor?: string, limit: number = 25): Promise<any> {
const params: Record<string, any> = { limit };
if (cursor) params.cursor = cursor;
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/labels`,
'GET',
null,
params
);
}
// Get attachments of a Confluence page (API v2, cursor-based)
export async function getConfluencePageAttachmentsV2(config: AtlassianConfig, pageId: string, cursor?: string, limit: number = 25): Promise<any> {
const params: Record<string, any> = { limit };
if (cursor) params.cursor = cursor;
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/attachments`,
'GET',
null,
params
);
}
// Get versions of a Confluence page (API v2, cursor-based)
export async function getConfluencePageVersionsV2(config: AtlassianConfig, pageId: string, cursor?: string, limit: number = 25): Promise<any> {
const params: Record<string, any> = { limit };
if (cursor) params.cursor = cursor;
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/versions`,
'GET',
null,
params
);
}
// Get list of Confluence pages (API v2, cursor-based)
export async function getConfluencePagesV2(config: AtlassianConfig, cursor?: string, limit: number = 25): Promise<any> {
const params: Record<string, any> = { limit };
if (cursor) params.cursor = cursor;
return await callConfluenceApi<any>(
config,
`/api/v2/pages`,
'GET',
null,
params
);
}
// Get Confluence page details (API v2, metadata only)
export async function getConfluencePageV2(config: AtlassianConfig, pageId: string): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}`,
'GET'
);
}
// Get Confluence page body (API v2)
export async function getConfluencePageBodyV2(config: AtlassianConfig, pageId: string): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/body`,
'GET'
);
}
// Get Confluence page ancestors (API v2)
export async function getConfluencePageAncestorsV2(config: AtlassianConfig, pageId: string): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/ancestors`,
'GET'
);
}
// Get list of Confluence spaces (API v2, cursor-based)
export async function getConfluenceSpacesV2(config: AtlassianConfig, cursor?: string, limit: number = 25): Promise<any> {
const params: Record<string, any> = { limit };
if (cursor) params.cursor = cursor;
return await callConfluenceApi<any>(
config,
`/api/v2/spaces`,
'GET',
null,
params
);
}
// Get Confluence space details (API v2)
export async function getConfluenceSpaceV2(config: AtlassianConfig, spaceKey: string): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/spaces/${encodeURIComponent(spaceKey)}`,
'GET'
);
}
// Get children of a Confluence page (API v2)
export async function getConfluencePageChildrenV2(config: AtlassianConfig, pageId: string): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/children`,
'GET'
);
}
// Get footer comments of a Confluence page (API v2)
export async function getConfluencePageFooterCommentsV2(config: AtlassianConfig, pageId: string, params: { limit?: number, cursor?: string } = {}): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/footer-comments`,
'GET',
null,
params
);
}
// Get inline comments of a Confluence page (API v2)
export async function getConfluencePageInlineCommentsV2(config: AtlassianConfig, pageId: string, params: { limit?: number, cursor?: string } = {}): Promise<any> {
return await callConfluenceApi<any>(
config,
`/api/v2/pages/${encodeURIComponent(pageId)}/inline-comments`,
'GET',
null,
params
);
}
// Get list of Confluence pages (API v2, hỗ trợ filter nâng cao)
export async function getConfluencePagesWithFilters(config: AtlassianConfig, filters: Record<string, any> = {}): Promise<any> {
return await callConfluenceApi<any>(
config,
'/api/v2/pages',
'GET',
null,
filters
);
}
```
--------------------------------------------------------------------------------
/src/resources/jira/filters.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Jira Filter Resources
*
* These resources provide access to Jira filters through MCP.
*/
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { filterListSchema, filterSchema } from '../../schemas/jira.js';
import { createStandardMetadata } from '../../schemas/common.js';
import { getFilters, getFilterById, getMyFilters } from '../../utils/jira-resource-api.js';
import { Logger } from '../../utils/logger.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';
const logger = Logger.getLogger('JiraFilterResources');
/**
* Register all Jira filter resources with MCP Server
* @param server MCP Server instance
*/
export function registerFilterResources(server: McpServer) {
logger.info('Registering Jira filter resources...');
// Chỉ đăng ký mỗi template một lần kèm handler
// Resource: Filter list
const filtersTemplate = new ResourceTemplate('jira://filters', {
list: async (_extra) => ({
resources: [
{
uri: 'jira://filters',
name: 'Jira Filters',
description: 'List and search all Jira filters',
mimeType: 'application/json'
}
]
})
});
// Resource: Filter details
const filterDetailsTemplate = new ResourceTemplate('jira://filters/{filterId}', {
list: async (_extra) => ({
resources: [
{
uri: 'jira://filters/{filterId}',
name: 'Jira Filter Details',
description: 'Get details for a specific Jira filter by ID. Replace {filterId} with the filter ID.',
mimeType: 'application/json'
}
]
})
});
// Resource: My filters
const myFiltersTemplate = new ResourceTemplate('jira://filters/my', {
list: async (_extra) => ({
resources: [
{
uri: 'jira://filters/my',
name: 'Jira My Filters',
description: 'List filters owned by or shared with the current user.',
mimeType: 'application/json'
}
]
})
});
// Đăng ký template kèm handler thực thi - chỉ đăng ký một lần mỗi URI
server.resource('jira-filters-list', filtersTemplate,
async (uri: string | URL, params: Record<string, any>, _extra: any) => {
try {
// Get config from environment
const config = Config.getAtlassianConfigFromEnv();
const { limit, offset } = Resources.extractPagingParams(params);
const response = await getFilters(config, offset, limit);
return Resources.createStandardResource(
typeof uri === 'string' ? uri : uri.href,
response.values,
'filters',
filterListSchema,
response.total || response.values.length,
limit,
offset,
`${config.baseUrl}/secure/ManageFilters.jspa`
);
} catch (error) {
logger.error('Error getting filter list:', error);
throw error;
}
}
);
server.resource('jira-filter-details', filterDetailsTemplate,
async (uri: string | URL, params: Record<string, any>, _extra: any) => {
try {
// Get config from environment
const config = Config.getAtlassianConfigFromEnv();
const filterId = Array.isArray(params.filterId) ? params.filterId[0] : params.filterId;
const filter = await getFilterById(config, filterId);
return Resources.createStandardResource(
typeof uri === 'string' ? uri : uri.href,
[filter],
'filter',
filterSchema,
1,
1,
0,
`${config.baseUrl}/secure/ManageFilters.jspa?filterId=${filterId}`
);
} catch (error) {
logger.error(`Error getting filter details for filter ${params.filterId}:`, error);
throw error;
}
}
);
server.resource('jira-my-filters', myFiltersTemplate,
async (uri: string | URL, _params: Record<string, any>, _extra: any) => {
try {
// Get config from environment
const config = Config.getAtlassianConfigFromEnv();
const filters = await getMyFilters(config);
return Resources.createStandardResource(
typeof uri === 'string' ? uri : uri.href,
filters,
'filters',
filterListSchema,
filters.length,
filters.length,
0,
`${config.baseUrl}/secure/ManageFilters.jspa?filterView=my`
);
} catch (error) {
logger.error('Error getting my filters:', error);
throw error;
}
}
);
logger.info('Jira filter resources registered successfully');
}
```
--------------------------------------------------------------------------------
/src/tools/confluence/create-page.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { callConfluenceApi } from '../../utils/atlassian-api-base.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { createConfluencePageV2 } from '../../utils/confluence-tool-api.js';
import { Config } from '../../utils/mcp-helpers.js';
// Initialize logger
const logger = Logger.getLogger('ConfluenceTools:createPage');
// Input parameter schema
export const createPageSchema = z.object({
spaceId: z.string().describe('Space ID (required, must be the numeric ID from API v2, NOT the key like TX, DEV, ...)'),
title: z.string().describe('Title of the page (required)'),
content: z.string().describe(`Content of the page (required, must be in Confluence storage format - XML-like HTML).
- Plain text or markdown is NOT supported (will throw error).
- Only XML-like HTML tags, Confluence macros (<ac:structured-macro>, <ac:rich-text-body>, ...), tables, panels, info, warning, etc. are supported if valid storage format.
- Content MUST strictly follow Confluence storage format.
Valid examples:
- <p>This is a paragraph</p>
- <ac:structured-macro ac:name="info"><ac:rich-text-body>Information</ac:rich-text-body></ac:structured-macro>
`),
parentId: z.string().describe('Parent page ID (required, must specify the parent page to create a child page)')
});
type CreatePageParams = z.infer<typeof createPageSchema>;
interface CreatePageResult {
id: string;
key: string;
title: string;
self: string;
webui: string;
success: boolean;
spaceId?: string;
}
// Main handler to create a new page (API v2)
export async function createPageHandler(
params: CreatePageParams,
config: AtlassianConfig
): Promise<CreatePageResult> {
try {
logger.info(`Creating new page (v2) "${params.title}" in spaceId ${params.spaceId}`);
const data = await createConfluencePageV2(config, {
spaceId: params.spaceId,
title: params.title,
content: params.content,
parentId: params.parentId
});
return {
id: data.id,
key: data.key || '',
title: data.title,
self: data._links.self,
webui: data._links.webui,
success: true,
spaceId: params.spaceId
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
logger.error(`Error creating page (v2) in spaceId ${params.spaceId}:`, error);
let message = `Failed to create page: ${error instanceof Error ? error.message : String(error)}`;
throw new ApiError(
ApiErrorType.SERVER_ERROR,
message,
500
);
}
}
// Register the tool with MCP Server
export const registerCreatePageTool = (server: McpServer) => {
server.tool(
'createPage',
'Create a new page in Confluence (API v2, chỉ hỗ trợ spaceId)',
createPageSchema.shape,
async (params: CreatePageParams, context: Record<string, any>) => {
try {
const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
if (!config) {
return {
content: [
{ type: 'text', text: 'Invalid or missing Atlassian configuration' }
],
isError: true
};
}
const result = await createPageHandler(params, config);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Page created successfully!`,
id: result.id,
title: result.title,
spaceId: result.spaceId
})
}
]
};
} catch (error) {
if (error instanceof ApiError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: error.message,
code: error.code,
statusCode: error.statusCode,
type: error.type
})
}
],
isError: true
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `Error while creating page: ${error instanceof Error ? error.message : String(error)}`
})
}
],
isError: true
};
}
}
);
};
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/list-mcp-inventory.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from "path";
import { fileURLToPath } from "url";
// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function main() {
const client = new Client({ name: "mcp-atlassian-inventory-list", version: "1.0.0" });
const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
const transport = new StdioClientTransport({
command: "node",
args: [serverPath],
env: process.env as Record<string, string>
});
console.log("Connecting to MCP server...");
await client.connect(transport);
// List available tools with details
console.log("\n=== Available Tools ===");
const toolsResult = await client.listTools();
console.log(`Total tools: ${toolsResult.tools.length}`);
toolsResult.tools.forEach((tool, index) => {
console.log(`${index + 1}. ${tool.name}: ${tool.description || 'No description'}`);
});
// List available resources
console.log("\n=== Available Resources ===");
let resourcesResult: any = { resources: [] };
try {
resourcesResult = await client.listResources();
console.log(`Total resources: ${resourcesResult.resources.length}`);
resourcesResult.resources.forEach((resource: any, index: number) => {
console.log(`${index + 1}. ${resource.uriPattern || resource.uri}: ${resource.description || 'No description'}`);
});
if (resourcesResult.resources.length === 0) {
console.warn("WARNING: No resources returned by listResources. This may indicate missing list callbacks in the MCP server resource registration.");
console.warn("Try these common resource URIs manually:");
[
'jira://issues',
'jira://projects',
'jira://boards',
'confluence://pages',
'confluence://spaces'
].forEach((uri, idx) => {
console.log(` ${idx + 1}. ${uri}`);
});
}
} catch (error) {
console.log("Error listing resources:", error instanceof Error ? error.message : String(error));
}
// Group tools by category
console.log("\n=== Tools by Category ===");
const toolsByCategory: Record<string, any[]> = {};
toolsResult.tools.forEach(tool => {
let category = "Other";
if (tool.name.startsWith("create") || tool.name.startsWith("update") ||
tool.name.startsWith("delete") || tool.name.startsWith("get")) {
if (tool.name.toLowerCase().includes("issue") || tool.name.toLowerCase().includes("sprint") ||
tool.name.toLowerCase().includes("board") || tool.name.toLowerCase().includes("filter")) {
category = "Jira";
} else if (tool.name.toLowerCase().includes("page") || tool.name.toLowerCase().includes("comment") ||
tool.name.toLowerCase().includes("space")) {
category = "Confluence";
}
}
if (!toolsByCategory[category]) toolsByCategory[category] = [];
toolsByCategory[category].push(tool);
});
Object.entries(toolsByCategory).forEach(([category, tools]) => {
console.log(`\n${category} Tools (${tools.length}):`);
tools.forEach((tool, index) => {
console.log(` ${index + 1}. ${tool.name}`);
});
});
// Group resources by category
console.log("\n=== Resources by Category ===");
const resourcesByCategory: Record<string, any[]> = {};
resourcesResult.resources.forEach((resource: any) => {
let category = "Other";
const uri = resource.uriPattern || resource.uri || "";
if (uri.startsWith("jira://")) {
category = "Jira";
} else if (uri.startsWith("confluence://")) {
category = "Confluence";
}
if (!resourcesByCategory[category]) resourcesByCategory[category] = [];
resourcesByCategory[category].push(resource);
});
Object.entries(resourcesByCategory).forEach(([category, resources]) => {
console.log(`\n${category} Resources (${resources.length}):`);
resources.forEach((resource: any, index: number) => {
const uri = resource.uriPattern || resource.uri || "";
console.log(` ${index + 1}. ${uri}`);
});
});
// Show details for some important tools
console.log("\n=== Tool Details ===");
const toolsToInspect = ["createIssue", "updatePage", "addComment"];
for (const toolName of toolsToInspect) {
const tool = toolsResult.tools.find(t => t.name === toolName);
if (tool) {
console.log(`\nTool: ${tool.name}`);
console.log(`Description: ${tool.description || 'No description'}`);
console.log("Input Schema:", JSON.stringify(tool.inputSchema, null, 2));
}
}
await client.close();
console.log("\nDone.");
}
main();
```
--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/test-confluence-pages.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from 'path';
import { fileURLToPath } from "url";
import fs from "fs";
// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load environment variables from .env
function loadEnv(): Record<string, string> {
try {
const envFile = path.resolve(process.cwd(), '.env');
const envContent = fs.readFileSync(envFile, 'utf8');
const envVars: Record<string, string> = {};
envContent.split('\n').forEach(line => {
if (line.trim().startsWith('#') || !line.trim()) return;
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=');
envVars[key.trim()] = value.trim();
}
});
return envVars;
} catch (error) {
console.error("Error loading .env file:", error);
return {};
}
}
// Print only response data
function printResourceMetaAndSchema(res: any) {
if (res.contents && res.contents.length > 0) {
const content = res.contents[0];
// COMMENTED OUT: Metadata and schema printing
// // Print metadata if exists
// if (content.metadata) {
// console.log("Metadata:", content.metadata);
// }
// // Print schema if exists
// if (content.schema) {
// console.log("Schema:", JSON.stringify(content.schema, null, 2));
// }
// Try to parse text if exists
if (content.text) {
try {
const data = JSON.parse(String(content.text));
console.log("Response Data:", JSON.stringify(data, null, 2));
} catch (e) {
console.log("Raw Response:", content.text);
}
}
}
}
async function main() {
const client = new Client({
name: "mcp-atlassian-test-client-confluence-pages",
version: "1.0.0"
});
// Path to MCP server
const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
// Load environment variables
const envVars = loadEnv();
const processEnv: Record<string, string> = {};
Object.keys(process.env).forEach(key => {
if (process.env[key] !== undefined) {
processEnv[key] = process.env[key] as string;
}
});
// Initialize transport
const transport = new StdioClientTransport({
command: "node",
args: [serverPath],
env: {
...processEnv,
...envVars
}
});
// Connect to server
console.log("Connecting to MCP server...");
await client.connect(transport);
console.log("\n=== Test Confluence Pages Resource ===");
// Change these values to match your environment if needed
const pageId = "19431426"; // Home page id mới cho space AWA1
const spaceKey = "AWA1"; // Space key mới
const resourceUris = [
`confluence://pages/${pageId}`,
`confluence://spaces/${spaceKey}/pages`,
`confluence://pages/${pageId}/children`,
`confluence://pages/${pageId}/comments`,
`confluence://pages/${pageId}/versions`,
`confluence://pages/${pageId}/ancestors`,
`confluence://pages/${pageId}/attachments`
];
for (const uri of resourceUris) {
try {
console.log(`\nResource: ${uri}`);
const res = await client.readResource({ uri });
if (uri.includes("?cql=")) {
const pagesData = JSON.parse(String(res.contents[0].text));
console.log("Number of pages from CQL:", pagesData.pages?.length || 0);
} else if (uri.includes("/children")) {
const childrenData = JSON.parse(String(res.contents[0].text));
console.log("Number of children:", childrenData.children?.length || 0);
} else if (uri.includes("/comments")) {
const commentsData = JSON.parse(String(res.contents[0].text));
console.log("Number of comments:", commentsData.comments?.length || 0);
} else if (uri.includes("/versions")) {
const versionsData = JSON.parse(String(res.contents[0].text));
console.log("Number of versions:", versionsData.versions?.length || 0);
} else if (uri.includes("/ancestors")) {
const ancestorsData = JSON.parse(String(res.contents[0].text));
console.log("Ancestors:", JSON.stringify(ancestorsData.ancestors, null, 2));
} else if (uri.includes("/attachments")) {
const attachmentsData = JSON.parse(String(res.contents[0].text));
console.log("Number of attachments:", attachmentsData.attachments?.length || 0);
}
printResourceMetaAndSchema(res);
} catch (e) {
console.error(`Resource ${uri} error:`, e instanceof Error ? e.message : e);
}
}
console.log("\n=== Finished testing Confluence Pages Resource! ===");
await client.close();
}
main();
```
--------------------------------------------------------------------------------
/src/utils/error-handler.ts:
--------------------------------------------------------------------------------
```typescript
import { Logger } from './logger.js';
// Initialize logger
const logger = Logger.getLogger('ErrorHandler');
/**
* API Error Type
*/
export enum ApiErrorType {
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
NOT_FOUND_ERROR = 'NOT_FOUND_ERROR',
RATE_LIMIT_ERROR = 'RATE_LIMIT_ERROR',
SERVER_ERROR = 'SERVER_ERROR',
NETWORK_ERROR = 'NETWORK_ERROR',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
RESOURCE_ERROR = 'RESOURCE_ERROR'
}
/**
* API Error
*/
export class ApiError extends Error {
readonly type: ApiErrorType;
readonly statusCode: number;
readonly code: string;
readonly originalError?: Error;
/**
* Initialize ApiError
* @param type Error type
* @param message Error message
* @param statusCode HTTP status code
* @param originalError Original error (optional)
*/
constructor(
type: ApiErrorType,
message: string,
statusCode: number = 500,
originalError?: Error
) {
super(message);
this.name = 'ApiError';
this.type = type;
this.statusCode = statusCode;
this.code = type; // Use ApiErrorType as code
this.originalError = originalError;
// Log error
logger.error(`${type}: ${message}`, {
statusCode,
code: this.code,
originalError: originalError?.message
});
}
/**
* Convert ApiError to JSON string
* @returns JSON representation of the error
*/
toJSON(): Record<string, any> {
return {
error: true,
type: this.type,
code: this.code,
message: this.message,
statusCode: this.statusCode
};
}
}
/**
* Handle error from Atlassian API
* @param error Error to handle
* @returns Normalized ApiError
*/
export function handleAtlassianError(error: any): ApiError {
// If already an ApiError, return it
if (error instanceof ApiError) {
return error;
}
// Handle HTTP error from Atlassian API
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
return new ApiError(
ApiErrorType.VALIDATION_ERROR,
data.message || 'Invalid data',
400,
error
);
case 401:
return new ApiError(
ApiErrorType.AUTHENTICATION_ERROR,
'Authentication failed. Please check your API token.',
401,
error
);
case 403:
return new ApiError(
ApiErrorType.AUTHORIZATION_ERROR,
'You do not have permission to access this resource.',
403,
error
);
case 404:
return new ApiError(
ApiErrorType.NOT_FOUND_ERROR,
'Requested resource not found.',
404,
error
);
case 429:
return new ApiError(
ApiErrorType.RATE_LIMIT_ERROR,
'Rate limit exceeded. Please try again later.',
429,
error
);
case 500:
case 502:
case 503:
case 504:
return new ApiError(
ApiErrorType.SERVER_ERROR,
'Atlassian server error.',
status,
error
);
default:
return new ApiError(
ApiErrorType.UNKNOWN_ERROR,
`Unknown error (${status})`,
status,
error
);
}
}
// Handle network error
if (error.request) {
return new ApiError(
ApiErrorType.NETWORK_ERROR,
'Cannot connect to Atlassian API.',
0,
error
);
}
// Other errors
return new ApiError(
ApiErrorType.UNKNOWN_ERROR,
error.message || 'Unknown error',
500,
error
);
}
/**
* Utility function to handle errors when calling API
* @param fn Function to handle errors for
* @returns Function with error handling
*/
export function withErrorHandling<T>(fn: () => Promise<T>): Promise<T> {
return fn().catch(error => {
throw handleAtlassianError(error);
});
}
/**
* Higher-order function that wraps a resource handler with error handling
* @param resourceName Name of the resource for logging purposes
* @param handler Resource handler function to wrap
* @returns Wrapped handler function with error handling
*/
export function wrapResourceWithErrorHandling<T, P>(
resourceName: string,
handler: (params: P) => Promise<T>
): (params: P) => Promise<T> {
return async (params: P): Promise<T> => {
try {
return await handler(params);
} catch (error) {
logger.error(`Error in resource ${resourceName}:`, error);
// Convert to ApiError if not already
const apiError = error instanceof ApiError
? error
: new ApiError(
ApiErrorType.RESOURCE_ERROR,
`Error processing resource ${resourceName}: ${error instanceof Error ? error.message : String(error)}`,
500,
error instanceof Error ? error : new Error(String(error))
);
throw apiError;
}
};
}
```
--------------------------------------------------------------------------------
/src/utils/confluence-interfaces.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Confluence API Interface
* Define data structures for Confluence API
*/
/**
* Confluence user information
*/
export interface ConfluenceUser {
accountId: string;
email?: string;
displayName: string;
publicName?: string;
profilePicture: {
path: string;
width: number;
height: number;
isDefault: boolean;
};
}
/**
* Space information
*/
export interface ConfluenceSpace {
id: string;
key: string;
name: string;
type: 'global' | 'personal';
status: 'current';
_expandable?: Record<string, string>;
_links?: Record<string, string>;
}
/**
* Version information
*/
export interface ConfluenceVersion {
by: ConfluenceUser;
when: string;
number: number;
message?: string;
minorEdit: boolean;
hidden: boolean;
}
/**
* Confluence content type
*/
export type ConfluenceContentType = 'page' | 'blogpost' | 'comment' | 'attachment';
/**
* Content body
*/
export interface ConfluenceBody {
storage: {
value: string;
representation: 'storage' | 'view' | 'export_view' | 'styled_view' | 'anonymous_export_view';
};
_expandable?: Record<string, string>;
}
/**
* Content information in Confluence
*/
export interface ConfluenceContent {
id: string;
type: ConfluenceContentType;
status: 'current' | 'trashed' | 'historical' | 'draft';
title: string;
space?: ConfluenceSpace;
version?: ConfluenceVersion;
body?: ConfluenceBody;
ancestors?: ConfluenceContent[];
children?: {
page?: {
results: ConfluenceContent[];
size: number;
};
comment?: {
results: ConfluenceContent[];
size: number;
};
attachment?: {
results: ConfluenceContent[];
size: number;
};
};
descendants?: {
page?: {
results: ConfluenceContent[];
size: number;
};
comment?: {
results: ConfluenceContent[];
size: number;
};
attachment?: {
results: ConfluenceContent[];
size: number;
};
};
container?: {
id: string;
type: ConfluenceContentType;
_links?: Record<string, string>;
};
metadata?: {
labels?: {
results: {
prefix: string;
name: string;
id: string;
}[];
size: number;
};
currentuser?: Record<string, any>;
properties?: Record<string, any>;
};
restrictions?: {
read?: {
restrictions: {
group?: {
name: string;
type: string;
};
user?: ConfluenceUser;
}[];
operation: 'read';
};
update?: {
restrictions: {
group?: {
name: string;
type: string;
};
user?: ConfluenceUser;
}[];
operation: 'update';
};
};
_expandable?: Record<string, string>;
_links?: Record<string, string>;
}
/**
* Parameters for creating new content
*/
export interface CreateContentParams {
type: ConfluenceContentType;
space: {
key: string;
};
title: string;
body: {
storage: {
value: string;
representation: 'storage';
};
};
ancestors?: {
id: string;
}[];
status?: 'current' | 'draft';
}
/**
* Parameters for updating content
*/
export interface UpdateContentParams {
type?: ConfluenceContentType;
title?: string;
body?: {
storage: {
value: string;
representation: 'storage';
};
};
version: {
number: number;
};
status?: 'current' | 'draft';
}
/**
* Parameters for searching spaces
*/
export interface SearchSpacesParams {
keys?: string[];
type?: 'global' | 'personal';
status?: 'current' | 'archived';
label?: string;
expand?: string[];
start?: number;
limit?: number;
}
/**
* Search result for spaces
*/
export interface SearchSpacesResult {
results: ConfluenceSpace[];
start: number;
limit: number;
size: number;
_links?: Record<string, string>;
}
/**
* Parameters for searching content
*/
export interface SearchContentParams {
cql: string;
cqlcontext?: Record<string, string>;
expand?: string[];
start?: number;
limit?: number;
}
/**
* Search result for content
*/
export interface SearchContentResult {
results: ConfluenceContent[];
start: number;
limit: number;
size: number;
totalSize?: number;
cqlQuery?: string;
searchDuration?: number;
_links?: Record<string, string>;
}
/**
* Information about a comment
*/
export interface ConfluenceComment {
id: string;
type: 'comment';
status: 'current' | 'trashed' | 'historical' | 'draft';
title: string;
body: ConfluenceBody;
version: ConfluenceVersion;
container: {
id: string;
type: ConfluenceContentType;
_links?: Record<string, string>;
};
_expandable?: Record<string, string>;
_links?: Record<string, string>;
}
/**
* Parameters for creating a comment
*/
export interface CreateCommentParams {
body: {
storage: {
value: string;
representation: 'storage';
};
};
container: {
id: string;
type: ConfluenceContentType;
};
status?: 'current' | 'draft';
}
```
--------------------------------------------------------------------------------
/src/utils/mcp-helpers.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Helper functions for MCP resources and tools
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpResponse, createJsonResponse, createErrorResponse, createSuccessResponse } from './mcp-core.js';
import { ApiError, ApiErrorType } from './error-handler.js';
import { AtlassianConfig } from './atlassian-api-base.js';
import { Logger } from './logger.js';
import { StandardMetadata, createStandardMetadata } from '../schemas/common.js';
const logger = Logger.getLogger('MCPHelpers');
/**
* Environment and configuration utilities
*/
export namespace Config {
/**
* Get Atlassian configuration from environment variables
*/
export function getAtlassianConfigFromEnv(): AtlassianConfig {
const ATLASSIAN_SITE_NAME = process.env.ATLASSIAN_SITE_NAME || '';
const ATLASSIAN_USER_EMAIL = process.env.ATLASSIAN_USER_EMAIL || '';
const ATLASSIAN_API_TOKEN = process.env.ATLASSIAN_API_TOKEN || '';
if (!ATLASSIAN_SITE_NAME || !ATLASSIAN_USER_EMAIL || !ATLASSIAN_API_TOKEN) {
logger.error('Missing Atlassian credentials in environment variables');
throw new Error('Missing Atlassian credentials in environment variables');
}
return {
baseUrl: ATLASSIAN_SITE_NAME.includes('.atlassian.net')
? `https://${ATLASSIAN_SITE_NAME}`
: ATLASSIAN_SITE_NAME,
email: ATLASSIAN_USER_EMAIL,
apiToken: ATLASSIAN_API_TOKEN
};
}
/**
* Helper to get Atlassian config from context or environment
*/
export function getConfigFromContextOrEnv(context: any): AtlassianConfig {
if (context?.atlassianConfig) {
return context.atlassianConfig;
}
return getAtlassianConfigFromEnv();
}
}
/**
* Resource helper functions
*/
export namespace Resources {
/**
* Create a standardized resource response with metadata and schema
*/
export function createStandardResource(
uri: string,
data: any[],
dataKey: string,
schema: any,
totalCount: number,
limit: number,
offset: number,
uiUrl?: string
): McpResponse {
// Create standard metadata
const metadata = createStandardMetadata(totalCount, limit, offset, uri, uiUrl);
// Create response data object
const responseData: Record<string, any> = {
metadata: metadata
};
// Add the data with the specified key
responseData[dataKey] = data;
// Return formatted resource
return createJsonResponse(uri, responseData);
}
/**
* Extract paging parameters from resource URI or request
*/
export function extractPagingParams(
params: any,
defaultLimit: number = 20,
defaultOffset: number = 0
): { limit: number, offset: number } {
let limit = defaultLimit;
let offset = defaultOffset;
if (params) {
// Extract limit
if (params.limit) {
const limitParam = Array.isArray(params.limit) ? params.limit[0] : params.limit;
const parsedLimit = parseInt(limitParam, 10);
if (!isNaN(parsedLimit) && parsedLimit > 0) {
limit = parsedLimit;
}
}
// Extract offset
if (params.offset) {
const offsetParam = Array.isArray(params.offset) ? params.offset[0] : params.offset;
const parsedOffset = parseInt(offsetParam, 10);
if (!isNaN(parsedOffset) && parsedOffset >= 0) {
offset = parsedOffset;
}
}
}
return { limit, offset };
}
}
/**
* Tool helper functions
*/
export namespace Tools {
/**
* Standardized response structure for MCP tools
*/
export interface ToolResponse<T = any> {
contents: Array<{
mimeType: string;
text: string;
}>;
isError?: boolean;
}
/**
* Create a standardized response for MCP tools
*/
export function createToolResponse<T = any>(success: boolean, message?: string, data?: T): ToolResponse<T> {
const response = {
success,
...(message && { message }),
...(data && { data })
};
return {
contents: [
{
mimeType: 'application/json',
text: JSON.stringify(response)
}
]
};
}
/**
* Higher-order function to wrap a tool implementation with standardized error handling
*/
export function wrapWithErrorHandling<T, P>(
toolName: string,
handler: (params: P) => Promise<T>
): (params: P) => Promise<ToolResponse<T>> {
return async (params: P): Promise<ToolResponse<T>> => {
try {
// Execute the handler
const result = await handler(params);
// Return successful response with data
return createToolResponse<T>(true, `${toolName} executed successfully`, result);
} catch (error) {
// Log the error
logger.error(`Error executing tool ${toolName}:`, error);
// Create appropriate error message
let errorMessage: string;
if (error instanceof ApiError) {
errorMessage = error.message;
} else {
errorMessage = error instanceof Error ? error.message : String(error);
}
// Return standardized error response
return createToolResponse(false, errorMessage);
}
};
}
}
```
--------------------------------------------------------------------------------
/src/utils/jira-interfaces.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Jira API Interface
* Define data structures for Jira API
*/
/**
* Interface defining data types for Jira API
*/
/**
* Define Jira user information
*/
export interface JiraUser {
accountId: string;
emailAddress?: string;
displayName: string;
active: boolean;
timeZone?: string;
accountType: string;
avatarUrls?: {
'48x48'?: string;
'24x24'?: string;
'16x16'?: string;
'32x32'?: string;
};
self: string;
}
/**
* Define Jira project information
*/
export interface JiraProject {
id: string;
key: string;
name: string;
self: string;
avatarUrls?: Record<string, string>;
projectCategory?: {
id: string;
name: string;
description?: string;
};
simplified?: boolean;
style?: string;
isPrivate?: boolean;
}
/**
* Define issue type
*/
export interface JiraIssueType {
id: string;
name: string;
description?: string;
iconUrl: string;
subtask: boolean;
avatarId?: number;
entityId?: string;
hierarchyLevel?: number;
self: string;
}
/**
* Define issue status
*/
export interface JiraStatus {
id: string;
name: string;
description?: string;
statusCategory: {
id: number;
key: string;
name: string;
colorName: string;
self: string;
};
self: string;
}
/**
* Define custom field
*/
export interface JiraCustomField {
id: string;
key?: string;
name?: string;
custom: boolean;
orderable: boolean;
navigable: boolean;
searchable: boolean;
clauseNames?: string[];
schema?: {
type: string;
custom?: string;
customId?: number;
items?: string;
};
}
/**
* Define issue priority
*/
export interface JiraPriority {
id: string;
name: string;
iconUrl: string;
self: string;
}
/**
* Define creator/updater
*/
export interface JiraUserDetails {
self: string;
accountId: string;
displayName: string;
active: boolean;
}
/**
* Define version/update information
*/
export interface JiraVersionInfo {
by: JiraUserDetails;
when: string;
}
/**
* Define rich text content
*/
export interface JiraContent {
type: string;
content?: JiraContent[];
text?: string;
attrs?: Record<string, any>;
}
/**
* Define content format
*/
export interface JiraBody {
type: string;
version: number;
content: JiraContent[];
}
/**
* Define comment
*/
export interface JiraComment {
id: string;
self: string;
body: any;
author: {
accountId: string;
displayName: string;
emailAddress?: string;
avatarUrls?: Record<string, string>;
};
created: string;
updated: string;
}
/**
* Define comment list
*/
export interface JiraComments {
comments: JiraComment[];
maxResults: number;
total: number;
startAt: number;
}
/**
* Define transition status
*/
export interface JiraTransition {
id: string;
name: string;
to: JiraStatus;
hasScreen: boolean;
isGlobal: boolean;
isInitial: boolean;
isConditional: boolean;
isAvailable: boolean;
}
/**
* Define transition result
*/
export interface JiraTransitionsResult {
transitions: {
id: string;
name: string;
to: {
id: string;
name: string;
statusCategory?: {
id: number;
key: string;
name: string;
};
};
}[];
}
/**
* Định nghĩa thông tin tệp đính kèm
*/
export interface JiraAttachment {
id: string;
filename: string;
author: JiraUserDetails;
created: string;
size: number;
mimeType: string;
content: string;
thumbnail?: string;
self: string;
}
/**
* Định nghĩa issue trong Jira
*/
export interface JiraIssue {
id: string;
key: string;
self: string;
fields: {
summary: string;
description?: any;
issuetype: {
id: string;
name: string;
iconUrl?: string;
};
project: {
id: string;
key: string;
name: string;
};
status?: {
id: string;
name: string;
statusCategory?: {
id: number;
key: string;
name: string;
};
};
priority?: {
id: string;
name: string;
};
labels?: string[];
assignee?: {
accountId: string;
displayName: string;
emailAddress?: string;
avatarUrls?: Record<string, string>;
};
reporter?: {
accountId: string;
displayName: string;
emailAddress?: string;
avatarUrls?: Record<string, string>;
};
created?: string;
updated?: string;
[key: string]: any;
};
changelog?: {
histories: {
id: string;
author: JiraUserDetails;
created: string;
items: {
field: string;
fieldtype: string;
from?: string;
fromString?: string;
to?: string;
toString?: string;
}[];
}[];
};
}
/**
* Định nghĩa kết quả tìm kiếm
*/
export interface JiraSearchResult {
startAt: number;
maxResults: number;
total: number;
issues: JiraIssue[];
}
/**
* Định nghĩa tham số tìm kiếm
*/
export interface JiraSearchParams {
jql: string;
startAt?: number;
maxResults?: number;
fields?: string[];
validateQuery?: boolean;
expand?: string[];
}
/**
* Định nghĩa tham số tạo issue
*/
export interface JiraCreateIssueParams {
fields: {
summary: string;
issuetype: {
id: string;
};
project: {
id: string;
};
description?: any;
[key: string]: any;
};
update?: any;
}
```
--------------------------------------------------------------------------------
/docs/dev-guide/resource-metadata-schema-guideline.md:
--------------------------------------------------------------------------------
```markdown
# Hướng Dẫn Chuẩn Hóa Metadata và Bổ Sung Schema cho MCP Server
## 1. Chuẩn Hóa Metadata Trả Về
### Tạo Cấu Trúc Metadata Nhất Quán
```typescript
// Định nghĩa interface chuẩn cho metadata
interface StandardMetadata {
total: number; // Tổng số bản ghi
limit: number; // Số bản ghi tối đa trả về
offset: number; // Vị trí bắt đầu
hasMore: boolean; // Còn dữ liệu không
links?: { // Các liên kết hữu ích
self: string; // Link đến resource hiện tại
ui?: string; // Link đến UI Atlassian
next?: string; // Link đến trang tiếp theo
}
}
// Hàm helper để tạo metadata chuẩn
function createStandardMetadata(
total: number,
limit: number,
offset: number,
baseUrl: string,
uiUrl?: string
): StandardMetadata {
const hasMore = offset + limit {
// Xử lý query parameters
const url = new URL(uri.href);
const limit = parseInt(url.searchParams.get("limit") || "20");
const offset = parseInt(url.searchParams.get("offset") || "0");
// Lấy dữ liệu từ Jira API
const issues = await jiraClient.getIssues(limit, offset);
const total = issues.total;
// Tạo metadata chuẩn
const metadata = createStandardMetadata(
total,
limit,
offset,
uri.href,
`https://${process.env.ATLASSIAN_SITE_NAME}/jira/issues`
);
// Trả về kết quả với metadata chuẩn
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
metadata,
issues: issues.issues
})
}]
};
}
);
```
## 2. Bổ Sung Schema Cho Resource MCP
### Định Nghĩa Schema Cho Resource
```typescript
// Định nghĩa schema cho issue
const issueSchema = {
type: "object",
properties: {
key: { type: "string", description: "Issue key (e.g., PROJ-123)" },
summary: { type: "string", description: "Issue title/summary" },
status: {
type: "object",
properties: {
name: { type: "string", description: "Status name" },
id: { type: "string", description: "Status ID" }
}
},
assignee: {
type: "object",
properties: {
displayName: { type: "string", description: "Assignee's display name" },
accountId: { type: "string", description: "Assignee's account ID" }
},
nullable: true
}
},
required: ["key", "summary", "status"]
};
// Schema cho danh sách issues
const issuesListSchema = {
type: "object",
properties: {
metadata: {
type: "object",
properties: {
total: { type: "number", description: "Total number of issues" },
limit: { type: "number", description: "Maximum number of issues returned" },
offset: { type: "number", description: "Starting position" },
hasMore: { type: "boolean", description: "Whether there are more issues" },
links: {
type: "object",
properties: {
self: { type: "string", description: "Link to this resource" },
ui: { type: "string", description: "Link to Atlassian UI" },
next: { type: "string", description: "Link to next page" }
}
}
},
required: ["total", "limit", "offset", "hasMore"]
},
issues: {
type: "array",
items: issueSchema
}
},
required: ["metadata", "issues"]
};
```
### Đăng Ký Resource Với Schema
```typescript
// Khi đăng ký resource với server
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "jira://issues",
name: "Jira Issues",
description: "List of Jira issues with pagination",
mimeType: "application/json",
schema: issuesListSchema // Thêm schema vào metadata resource
},
// Các resource khác...
]
}));
```
### Trả Về Schema Trong Response
```typescript
// Trong handler của resource
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "jira://issues") {
// Xử lý logic lấy dữ liệu...
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(responseData),
schema: issuesListSchema // Thêm schema vào response
}]
};
}
// Xử lý các resource khác...
});
```
## 3. Áp Dụng Cho Tất Cả Resource
Để áp dụng nhất quán cho tất cả resource, bạn nên:
1. **Tạo thư viện schema**: Tạo file riêng chứa tất cả schema (ví dụ: `schemas/jira.ts`, `schemas/confluence.ts`)
2. **Tạo helper function**: Viết các hàm helper để tạo metadata chuẩn và response chuẩn
3. **Áp dụng cho tất cả resource handler**: Đảm bảo mọi resource đều sử dụng cấu trúc và helper giống nhau
```typescript
// Ví dụ helper function
function createResourceResponse(uri: string, data: any, schema: any) {
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(data),
schema
}]
};
}
```
## 4. Kiểm Tra Với Cline
Sau khi triển khai, hãy kiểm tra với Cline để đảm bảo:
- Cline hiển thị đúng kiểu dữ liệu (không còn "Returns Unknown")
- Cline có thể render UI thông minh dựa trên schema
- Metadata được hiển thị và sử dụng đúng (phân trang, liên kết, v.v.)
Việc chuẩn hóa này sẽ giúp MCP server của bạn chuyên nghiệp hơn, dễ sử dụng với AI agent, và tương thích tốt hơn với hệ sinh thái MCP.
```
--------------------------------------------------------------------------------
/docs/dev-guide/modelcontextprotocol-introduction.md:
--------------------------------------------------------------------------------
```markdown
https://modelcontextprotocol.io/introduction
# Introduction
> Get started with the Model Context Protocol (MCP)
<Note>C# SDK released! Check out [what else is new.](/development/updates)</Note>
MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.
## Why MCP?
MCP helps you build agents and complex workflows on top of LLMs. LLMs frequently need to integrate with data and tools, and MCP provides:
* A growing list of pre-built integrations that your LLM can directly plug into
* The flexibility to switch between LLM providers and vendors
* Best practices for securing your data within your infrastructure
### General architecture
At its core, MCP follows a client-server architecture where a host application can connect to multiple servers:
```mermaid
flowchart LR
subgraph "Your Computer"
Host["Host with MCP Client\n(Claude, IDEs, Tools)"]
S1["MCP Server A"]
S2["MCP Server B"]
S3["MCP Server C"]
Host <-->|"MCP Protocol"| S1
Host <-->|"MCP Protocol"| S2
Host <-->|"MCP Protocol"| S3
S1 <--> D1[("Local\nData Source A")]
S2 <--> D2[("Local\nData Source B")]
end
subgraph "Internet"
S3 <-->|"Web APIs"| D3[("Remote\nService C")]
end
```
* **MCP Hosts**: Programs like Claude Desktop, IDEs, or AI tools that want to access data through MCP
* **MCP Clients**: Protocol clients that maintain 1:1 connections with servers
* **MCP Servers**: Lightweight programs that each expose specific capabilities through the standardized Model Context Protocol
* **Local Data Sources**: Your computer's files, databases, and services that MCP servers can securely access
* **Remote Services**: External systems available over the internet (e.g., through APIs) that MCP servers can connect to
## Get started
Choose the path that best fits your needs:
#### Quick Starts
<CardGroup cols={2}>
<Card title="For Server Developers" icon="bolt" href="/quickstart/server">
Get started building your own server to use in Claude for Desktop and other clients
</Card>
<Card title="For Client Developers" icon="bolt" href="/quickstart/client">
Get started building your own client that can integrate with all MCP servers
</Card>
<Card title="For Claude Desktop Users" icon="bolt" href="/quickstart/user">
Get started using pre-built servers in Claude for Desktop
</Card>
</CardGroup>
#### Examples
<CardGroup cols={2}>
<Card title="Example Servers" icon="grid" href="/examples">
Check out our gallery of official MCP servers and implementations
</Card>
<Card title="Example Clients" icon="cubes" href="/clients">
View the list of clients that support MCP integrations
</Card>
</CardGroup>
## Tutorials
<CardGroup cols={2}>
<Card title="Building MCP with LLMs" icon="comments" href="/tutorials/building-mcp-with-llms">
Learn how to use LLMs like Claude to speed up your MCP development
</Card>
<Card title="Debugging Guide" icon="bug" href="/docs/tools/debugging">
Learn how to effectively debug MCP servers and integrations
</Card>
<Card title="MCP Inspector" icon="magnifying-glass" href="/docs/tools/inspector">
Test and inspect your MCP servers with our interactive debugging tool
</Card>
<Card title="MCP Workshop (Video, 2hr)" icon="person-chalkboard" href="https://www.youtube.com/watch?v=kQmXtrmQ5Zg">
<iframe src="https://www.youtube.com/embed/kQmXtrmQ5Zg" />
</Card>
</CardGroup>
## Explore MCP
Dive deeper into MCP's core concepts and capabilities:
<CardGroup cols={2}>
<Card title="Core architecture" icon="sitemap" href="/docs/concepts/architecture">
Understand how MCP connects clients, servers, and LLMs
</Card>
<Card title="Resources" icon="database" href="/docs/concepts/resources">
Expose data and content from your servers to LLMs
</Card>
<Card title="Prompts" icon="message" href="/docs/concepts/prompts">
Create reusable prompt templates and workflows
</Card>
<Card title="Tools" icon="wrench" href="/docs/concepts/tools">
Enable LLMs to perform actions through your server
</Card>
<Card title="Sampling" icon="robot" href="/docs/concepts/sampling">
Let your servers request completions from LLMs
</Card>
<Card title="Transports" icon="network-wired" href="/docs/concepts/transports">
Learn about MCP's communication mechanism
</Card>
</CardGroup>
## Contributing
Want to contribute? Check out our [Contributing Guide](/development/contributing) to learn how you can help improve MCP.
## Support and Feedback
Here's how to get help or provide feedback:
* For bug reports and feature requests related to the MCP specification, SDKs, or documentation (open source), please [create a GitHub issue](https://github.com/modelcontextprotocol)
* For discussions or Q\&A about the MCP specification, use the [specification discussions](https://github.com/modelcontextprotocol/specification/discussions)
* For discussions or Q\&A about other MCP open source components, use the [organization discussions](https://github.com/orgs/modelcontextprotocol/discussions)
* For bug reports, feature requests, and questions related to Claude.app and claude.ai's MCP integration, please see Anthropic's guide on [How to Get Support](https://support.anthropic.com/en/articles/9015913-how-to-get-support)
```