Parcourir la source

feat: add edit functionality, testing, linting, and CI/CD

- Add create_page and edit_page MCP tools with WIKIJS_ENABLE_EDIT flag
- Set up ESLint with TypeScript support
- Add Jest testing framework with sample tests
- Create GitHub Actions workflows for CI, releases, and version bumps
- Update package.json with lint, test, and prepublish scripts
- Add .npmignore for cleaner npm packages
- Fix GraphQL mutations to handle WikiJS v2 quirks
- Add comprehensive documentation in README and CLAUDE.md
Test User il y a 1 an
Parent
commit
b9390c0662

+ 30 - 0
.eslintrc.json

@@ -0,0 +1,30 @@
+{
+  "root": true,
+  "parser": "@typescript-eslint/parser",
+  "parserOptions": {
+    "ecmaVersion": 2022,
+    "sourceType": "module",
+    "project": "./tsconfig.json"
+  },
+  "plugins": ["@typescript-eslint"],
+  "extends": [
+    "eslint:recommended",
+    "plugin:@typescript-eslint/recommended",
+    "plugin:@typescript-eslint/recommended-requiring-type-checking"
+  ],
+  "rules": {
+    "@typescript-eslint/explicit-function-return-type": "off",
+    "@typescript-eslint/explicit-module-boundary-types": "off",
+    "@typescript-eslint/no-explicit-any": "error",
+    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
+    "@typescript-eslint/no-floating-promises": "error",
+    "no-console": ["warn", { "allow": ["warn", "error"] }]
+  },
+  "ignorePatterns": [
+    "build/",
+    "node_modules/",
+    "*.js",
+    "*.d.ts",
+    "src/wikijs/generated/"
+  ]
+}

+ 50 - 0
.github/workflows/ci.yml

@@ -0,0 +1,50 @@
+name: CI
+
+on:
+  push:
+    branches: [ master, main ]
+  pull_request:
+    branches: [ master, main ]
+
+jobs:
+  lint-and-test:
+    runs-on: ubuntu-latest
+    
+    strategy:
+      matrix:
+        node-version: [18.x, 20.x, 22.x]
+    
+    steps:
+    - uses: actions/checkout@v4
+    
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v4
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'npm'
+    
+    - name: Install dependencies
+      run: npm ci
+    
+    - name: Run linter
+      run: npm run lint
+    
+    - name: Run tests
+      run: npm test
+    
+    - name: Upload coverage reports
+      uses: codecov/codecov-action@v4
+      if: matrix.node-version == '20.x'
+      with:
+        file: ./coverage/lcov.info
+        flags: unittests
+        name: codecov-umbrella
+        fail_ci_if_error: false
+    
+    - name: Build
+      run: npm run build
+    
+    - name: Test built executable
+      run: |
+        chmod +x build/index.js
+        node build/index.js --version || true

+ 81 - 0
.github/workflows/release.yml

@@ -0,0 +1,81 @@
+name: Release & Publish
+
+on:
+  push:
+    tags:
+      - 'v*'
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Version to release (e.g., 1.2.3)'
+        required: true
+        type: string
+
+permissions:
+  contents: write
+  packages: write
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+    
+    - name: Use Node.js 20.x
+      uses: actions/setup-node@v4
+      with:
+        node-version: '20.x'
+        registry-url: 'https://registry.npmjs.org'
+        cache: 'npm'
+    
+    - name: Install dependencies
+      run: npm ci
+    
+    - name: Run tests
+      run: npm test
+    
+    - name: Run linter
+      run: npm run lint
+    
+    - name: Build
+      run: npm run build
+    
+    - name: Set version from input
+      if: github.event_name == 'workflow_dispatch'
+      run: |
+        npm version ${{ github.event.inputs.version }} --no-git-tag-version
+        echo "VERSION=v${{ github.event.inputs.version }}" >> $GITHUB_ENV
+    
+    - name: Set version from tag
+      if: github.event_name == 'push'
+      run: |
+        echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+        npm version ${GITHUB_REF#refs/tags/v} --no-git-tag-version
+    
+    - name: Create Release
+      uses: softprops/action-gh-release@v2
+      with:
+        tag_name: ${{ env.VERSION }}
+        name: Release ${{ env.VERSION }}
+        body: |
+          ## What's Changed
+          
+          **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ github.event.before }}...${{ env.VERSION }}
+        draft: false
+        prerelease: false
+    
+    - name: Publish to npm
+      run: npm publish
+      env:
+        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+    
+    - name: Commit version bump
+      if: github.event_name == 'workflow_dispatch'
+      run: |
+        git config --global user.name 'github-actions[bot]'
+        git config --global user.email 'github-actions[bot]@users.noreply.github.com'
+        git add package.json package-lock.json
+        git commit -m "chore: bump version to ${{ github.event.inputs.version }}"
+        git push

+ 63 - 0
.github/workflows/version-bump.yml

@@ -0,0 +1,63 @@
+name: Version Bump
+
+on:
+  workflow_dispatch:
+    inputs:
+      bump:
+        description: 'Type of version bump'
+        required: true
+        type: choice
+        options:
+          - patch
+          - minor
+          - major
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  bump-version:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        token: ${{ secrets.GITHUB_TOKEN }}
+    
+    - name: Use Node.js 20.x
+      uses: actions/setup-node@v4
+      with:
+        node-version: '20.x'
+        cache: 'npm'
+    
+    - name: Configure Git
+      run: |
+        git config --global user.name 'github-actions[bot]'
+        git config --global user.email 'github-actions[bot]@users.noreply.github.com'
+    
+    - name: Install dependencies
+      run: npm ci
+    
+    - name: Bump version
+      run: |
+        npm version ${{ github.event.inputs.bump }} -m "chore: bump version to %s"
+        echo "NEW_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
+    
+    - name: Push changes
+      run: |
+        git push origin HEAD:refs/heads/version-bump-${{ env.NEW_VERSION }}
+        git push origin --tags
+    
+    - name: Create Pull Request
+      uses: peter-evans/create-pull-request@v6
+      with:
+        token: ${{ secrets.GITHUB_TOKEN }}
+        branch: version-bump-${{ env.NEW_VERSION }}
+        title: 'chore: bump version to ${{ env.NEW_VERSION }}'
+        body: |
+          This PR bumps the version to ${{ env.NEW_VERSION }}.
+          
+          After merging, a new release will be created automatically.
+        labels: |
+          version-bump
+          automated

+ 16 - 0
.mcp.json

@@ -0,0 +1,16 @@
+{
+  "mcpServers": {
+    "wikijs": {
+      "type": "stdio",
+      "command": "node",
+      "args": [
+        "build/index.js"
+      ],
+      "env": {
+        "WIKIJS_URL": "http://localhost:3000",
+        "WIKIJS_API_KEY": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGkiOjEsImdycCI6MSwiaWF0IjoxNzUzMjkyOTA0LCJleHAiOjE3ODQ4NTA1MDQsImF1ZCI6InVybjp3aWtpLmpzIiwiaXNzIjoidXJuOndpa2kuanMifQ.M970-p-Z254-qCuPnbzPZ4UELzfk1AEaKKyu3wTeb8on9mrAZLSUc6r8_q4efUakChfD7prhCrRQJpq6ccvGeV894GwkCoh9qtveIrlFkaAehn91hieDu8imOC2GvvsHGW3pGH_tv16xYIrQ2gyq5Kxkn3OPfqqoAiaO41_VAGhvlMlRDm6EQYshrbIV7fANwRckH0JdhLQOTCfi9-31_Wzv_taal6a-3W00cpK2SQ3DJBL-fI-m-p_mMDzELfQ6Fp2SfvfYaNFpC45EnE_yrXbnIxRlhJCD9gCYQhJ9nGjChjVhaDyBMmN9FR55qiNa1-soqkKFlRue4zgjth9iNw",
+        "WIKIJS_ENABLE_EDIT": "true"
+      }
+    }
+  }
+}

+ 45 - 0
.npmignore

@@ -0,0 +1,45 @@
+# Source files
+src/
+*.ts
+!build/**/*.d.ts
+
+# Development files
+.env
+.env.local
+.env.*
+docker-compose.yml
+Makefile
+codegen.yml
+tsconfig.json
+.mcp.json
+
+# Documentation
+CLAUDE.md
+
+# Git
+.git/
+.gitignore
+.github/
+
+# Dependencies
+node_modules/
+
+# IDE
+.vscode/
+.idea/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+
+# Testing
+__tests__/
+*.test.ts
+*.spec.ts
+jest.config.js
+.eslintrc.json
+coverage/

+ 74 - 0
CLAUDE.md

@@ -0,0 +1,74 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commands
+
+### Build Commands
+- `npm run build` - Compiles TypeScript to JavaScript and sets executable permissions
+- `make build` - Alternative build command using Makefile
+- `npm run codegen` - Generates TypeScript types from WikiJS GraphQL schema (requires WIKIJS_URL in .env)
+
+### Development Commands
+- `make run` - Builds and runs the MCP server
+- `make clean` - Removes build artifacts
+- `docker-compose up -d` - Starts a local WikiJS instance for testing
+- `docker-compose down` - Stops the local WikiJS instance
+
+## Architecture
+
+This is a Model Context Protocol (MCP) server that bridges AI assistants with WikiJS instances. The architecture follows a three-layer pattern:
+
+### Core Components
+
+1. **MCP Layer** (`src/mcp/`)
+   - `WikiJSMcpServer` class manages the MCP server lifecycle and tool registration
+   - Tools are registered in `src/mcp/index.ts:27-54` with specific parameter schemas
+   - Each tool is a separate module in `src/mcp/tools/`
+
+2. **WikiJS Client Layer** (`src/wikijs/`)
+   - `WikiJSClient` class wraps GraphQL operations
+   - Uses generated GraphQL types from `src/wikijs/generated/graphql.ts`
+   - GraphQL queries are defined in `src/wikijs/queries/pages.graphql`
+
+3. **Entry Point** (`src/index.ts`)
+   - Initializes WikiJS client with URL and API key from environment
+   - Creates MCP server instance and connects via stdio transport
+   - Handles fatal errors and process exit
+
+### Tool Architecture
+
+Each MCP tool follows a consistent pattern:
+- Exports a `createTool` function that returns a handler
+- Exports a `PARAMETERS` schema for validation
+- Uses Zod for runtime parameter validation
+- Returns structured responses with content arrays
+
+### GraphQL Integration
+
+The project uses GraphQL Code Generator to create typed SDK from WikiJS API:
+- Schema is fetched from `${WIKIJS_URL}/graphql`
+- Queries in `.graphql` files generate TypeScript types
+- SDK provides type-safe methods for WikiJS operations
+
+## Environment Configuration
+
+Required environment variables (set in `.env`):
+- `WIKIJS_URL` - Base URL of WikiJS instance (without /graphql)
+- `WIKIJS_API_KEY` - Bearer token for WikiJS API authentication
+
+## Type Safety
+
+The project uses TypeScript with strict typing:
+- Generated GraphQL types ensure API contract compliance
+- Zod schemas validate MCP tool parameters at runtime
+- All async operations are properly typed
+
+## MCP Tools Available
+
+- `search_pages` - Full-text search across WikiJS pages
+- `get_page_by_id` - Retrieve specific page by numeric ID
+- `get_page_by_path` - Retrieve page by path and locale
+- `get_all_pages` - List all pages with optional filters
+- `create_page` - Create a new WikiJS page (only available when WIKIJS_ENABLE_EDIT=true)
+- `edit_page` - Edit a WikiJS page by ID (only available when WIKIJS_ENABLE_EDIT=true)

+ 178 - 24
README.md

@@ -9,20 +9,96 @@ This MCP server enables AI assistants to interact with WikiJS instances by provi
 - Retrieve pages by ID
 - Retrieve pages by ID
 - Retrieve pages by path and locale
 - Retrieve pages by path and locale
 - Get all pages from the wiki
 - Get all pages from the wiki
+- Edit pages (when enabled)
+
+## Installation
+
+### Using npx (recommended)
+```bash
+npx wikijs-mcp
+```
+
+### From source
+```bash
+git clone https://github.com/RicardoCenci/wikijs-mcp.git
+cd wikijs-mcp
+npm install
+npm run build
+```
+
+## Configuration
+
+### Claude Code
+
+#### Method 1: Using the CLI (Recommended)
+```bash
+# Add as a user-scoped server (available across all projects)
+claude mcp add wikijs \
+  -e WIKIJS_URL=https://your-wiki.example.com \
+  -e WIKIJS_API_KEY=your-api-key \
+  -- npx wikijs-mcp
+
+# Or with edit functionality enabled
+claude mcp add wikijs \
+  -e WIKIJS_URL=https://your-wiki.example.com \
+  -e WIKIJS_API_KEY=your-api-key \
+  -e WIKIJS_ENABLE_EDIT=true \
+  -- npx wikijs-mcp
+```
+
+#### Method 2: Project-scoped configuration
+Create a `.mcp.json` file in your project root:
+```json
+{
+  "mcpServers": {
+    "wikijs": {
+      "command": "npx",
+      "args": ["wikijs-mcp"],
+      "env": {
+        "WIKIJS_URL": "https://your-wiki.example.com",
+        "WIKIJS_API_KEY": "your-api-key",
+        "WIKIJS_ENABLE_EDIT": "false"
+      }
+    }
+  }
+}
+```
+
+### Cursor
+Add to your Cursor settings (`.cursor/settings.json`):
+```json
+{
+  "mcpServers": {
+    "wikijs-mcp": {
+      "command": "npx",
+      "args": ["wikijs-mcp"],
+      "env": {
+        "WIKIJS_URL": "https://your-wiki.example.com",
+        "WIKIJS_API_KEY": "your-api-key",
+        "WIKIJS_ENABLE_EDIT": "false"
+      }
+    }
+  }
+}
+```
+
+### Claude Desktop
+Add to your Claude Desktop configuration:
+- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
+- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
+- Linux: `~/.config/Claude/claude_desktop_config.json`
 
 
-## Configuration for Cursor
 ```json
 ```json
 {
 {
   "mcpServers": {
   "mcpServers": {
     "wikijs-mcp": {
     "wikijs-mcp": {
       "command": "npx",
       "command": "npx",
-        "args": [
-            "wikijs-mcp"
-        ],
-        "env": {
-            "WIKIJS_URL": <your wikijs url>,
-            "WIKIJS_API_KEY": <your wikijs api key>
-        }
+      "args": ["wikijs-mcp"],
+      "env": {
+        "WIKIJS_URL": "https://your-wiki.example.com",
+        "WIKIJS_API_KEY": "your-api-key",
+        "WIKIJS_ENABLE_EDIT": "false"
+      }
     }
     }
   }
   }
 }
 }
@@ -32,15 +108,55 @@ This MCP server enables AI assistants to interact with WikiJS instances by provi
 
 
 1. Log into your WikiJS instance as an administrator
 1. Log into your WikiJS instance as an administrator
 2. Go to **Administration** > **API Access**
 2. Go to **Administration** > **API Access**
-3. Create a new API key with appropriate permissions
-4. Copy the generated key to your `.env` file
+3. Enable the API if not already enabled
+4. Click **New API Key**
+5. Give it a name (e.g., "MCP Server")
+6. Select **Full Access** or configure specific permissions
+7. Set an appropriate expiration date
+8. Copy the generated key immediately (it won't be shown again)
+
+## Environment Variables
 
 
+| Variable | Description | Required | Default |
+|----------|-------------|----------|---------|
+| `WIKIJS_URL` | URL of your WikiJS instance | Yes | - |
+| `WIKIJS_API_KEY` | WikiJS API key | Yes | - |
+| `WIKIJS_ENABLE_EDIT` | Enable page editing functionality | No | false |
+
+## Available Tools
+
+Once configured, the following tools will be available:
+
+- **search_pages** - Search for pages by query string
+  - Parameters: `query` (required), `path` (optional), `locale` (optional)
+  
+- **get_page_by_id** - Retrieve a specific page by its ID
+  - Parameters: `id` (required)
+  
+- **get_page_by_path** - Retrieve a page by its path and locale
+  - Parameters: `path` (required), `locale` (required)
+  
+- **get_all_pages** - List all pages with optional filters
+  - Parameters: `limit` (optional), `locale` (optional), `tags` (optional)
+  
+- **create_page** - Create a new WikiJS page (only when `WIKIJS_ENABLE_EDIT=true`)
+  - Parameters: `title` (required), `content` (required), `description` (required), `path` (required), `editor` (optional), `isPublished` (optional), `isPrivate` (optional), `locale` (optional), `tags` (optional)
+  
+- **edit_page** - Edit a WikiJS page (only when `WIKIJS_ENABLE_EDIT=true`)
+  - Parameters: `id` (required), `title`, `content`, `description`, `editor`, `isPublished`, `isPrivate`, `locale`, `tags`, `path`
 
 
 ## Development
 ## Development
 
 
+### Prerequisites
+- Node.js 18+
+- Docker (for local WikiJS testing)
+- npm or yarn
+
+### Setup
+
 1. Clone the repository:
 1. Clone the repository:
 ```bash
 ```bash
-git clone https://github.com/RicardoCenci/wikijs-mcp.git
+git clone https://github.com/cahaseler/wikijs-mcp.git
 cd wikijs-mcp
 cd wikijs-mcp
 ```
 ```
 
 
@@ -49,35 +165,73 @@ cd wikijs-mcp
 npm install
 npm install
 ```
 ```
 
 
-3. Copy the environment template and fill out its contents
+3. Copy the environment template:
 ```bash
 ```bash
 cp env.example .env
 cp env.example .env
 ```
 ```
 
 
-4. Build the project
-If you have `make` installed:
+4. Edit `.env` with your WikiJS credentials
+
+### Local WikiJS Instance
+
+To run a local WikiJS instance for testing:
+
 ```bash
 ```bash
+# Start WikiJS and PostgreSQL
+docker-compose up -d
+
+# The instance will be available at http://localhost:3000
+# Follow the setup wizard to create an admin account
+```
+
+### Building
+
+```bash
+# Using npm
+npm run build
+
+# Using make
 make build
 make build
 ```
 ```
 
 
-5. Deploy the WikiJS instance for testing
+### Testing the MCP Server
 
 
 ```bash
 ```bash
-docker composer up -d
+# Run the built server
+node build/index.js
+
+# Or use the make command
+make run
 ```
 ```
 
 
-## Usage
+## Security Considerations
+
+- **API Key Storage**: Never commit your API key to version control
+- **Edit Permissions**: The edit functionality is disabled by default. Only enable it if you trust the environment where the MCP server is running
+- **Scope Permissions**: When creating the WikiJS API key, consider using the minimum required permissions instead of full access
+
+## Troubleshooting
+
+### "WIKIJS_URL and WIKIJS_API_KEY must be set"
+Ensure your environment variables are properly configured. Check your `.env` file or the environment variables in your MCP client configuration.
 
 
+### "Failed to update page: Unauthorized"
+Your API key may be expired or lack the necessary permissions. Create a new API key with appropriate permissions.
+
+### GraphQL errors during development
+If you modify the GraphQL queries, regenerate the types:
 ```bash
 ```bash
-npx wikijs-mcp
+npm run codegen
 ```
 ```
 
 
-## Environment Variables
-| Variable | Description | Required | Default |
-|----------|-------------|----------|---------|
-| `WIKIJS_URL` | URL of your WikiJS instance | Yes | - |
-| `WIKIJS_API_KEY` | WikiJS API key | Yes | - |
+## Contributing
 
 
+1. Fork the repository
+2. Create your feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
 
 
 ## License
 ## License
-This project is licensed under the MIT License.
+
+This project is licensed under the MIT License - see the LICENSE file for details.

+ 2 - 1
env.example

@@ -1,2 +1,3 @@
 WIKIJS_URL=http://localhost:3000
 WIKIJS_URL=http://localhost:3000
-WIKIJS_API_KEY= 
+WIKIJS_API_KEY=
+WIKIJS_ENABLE_EDIT=false 

+ 22 - 0
jest.config.mjs

@@ -0,0 +1,22 @@
+/** @type {import('jest').Config} */
+export default {
+  preset: 'ts-jest/presets/default-esm',
+  testEnvironment: 'node',
+  roots: ['<rootDir>/src'],
+  testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
+  transform: {
+    '^.+\\.ts$': ['ts-jest', {
+      useESM: true,
+    }],
+  },
+  collectCoverageFrom: [
+    'src/**/*.ts',
+    '!src/**/*.d.ts',
+    '!src/wikijs/generated/**',
+    '!src/index.ts',
+  ],
+  moduleNameMapper: {
+    '^(\\.{1,2}/.*)\\.js$': '$1',
+  },
+  extensionsToTreatAsEsm: ['.ts'],
+};

Fichier diff supprimé car celui-ci est trop grand
+ 3524 - 1001
package-lock.json


+ 16 - 4
package.json

@@ -8,12 +8,12 @@
 	"license": "MIT",
 	"license": "MIT",
 	"repository": {
 	"repository": {
 		"type": "git",
 		"type": "git",
-		"url": "git+https://github.com/RicardoCenci/wikijs-mcp.git"
+		"url": "git+https://github.com/cahaseler/wikijs-mcp.git"
 	},
 	},
 	"bugs": {
 	"bugs": {
-		"url": "https://github.com/RicardoCenci/wikijs-mcp/issues"
+		"url": "https://github.com/cahaseler/wikijs-mcp/issues"
 	},
 	},
-	"homepage": "https://github.com/RicardoCenci/wikijs-mcp#readme",
+	"homepage": "https://github.com/cahaseler/wikijs-mcp#readme",
 	"keywords": ["mcp", "wikijs", "model-context-protocol", "ai", "assistant"],
 	"keywords": ["mcp", "wikijs", "model-context-protocol", "ai", "assistant"],
 	"files": [
 	"files": [
 		"build/**/*",
 		"build/**/*",
@@ -25,7 +25,13 @@
 	},
 	},
 	"scripts": {
 	"scripts": {
 		"build": "tsc && chmod 755 build/index.js",
 		"build": "tsc && chmod 755 build/index.js",
-		"codegen": "dotenv -- graphql-codegen --config codegen.yml"
+		"codegen": "dotenv -- graphql-codegen --config codegen.yml",
+		"lint": "eslint src --ext .ts",
+		"lint:fix": "eslint src --ext .ts --fix",
+		"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
+		"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
+		"test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage",
+		"prepublishOnly": "npm run lint && npm run test && npm run build"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
 		"@modelcontextprotocol/sdk": "^1.12.3",
 		"@modelcontextprotocol/sdk": "^1.12.3",
@@ -40,7 +46,13 @@
 		"@graphql-codegen/typescript": "^4.1.6",
 		"@graphql-codegen/typescript": "^4.1.6",
 		"@graphql-codegen/typescript-graphql-request": "^6.3.0",
 		"@graphql-codegen/typescript-graphql-request": "^6.3.0",
 		"@graphql-codegen/typescript-operations": "^4.6.1",
 		"@graphql-codegen/typescript-operations": "^4.6.1",
+		"@types/jest": "^29.5.0",
 		"@types/node": "^24.0.1",
 		"@types/node": "^24.0.1",
+		"@typescript-eslint/eslint-plugin": "^8.0.0",
+		"@typescript-eslint/parser": "^8.0.0",
+		"eslint": "^8.57.0",
+		"jest": "^29.5.0",
+		"ts-jest": "^29.1.0",
 		"typescript": "^5.8.3"
 		"typescript": "^5.8.3"
 	}
 	}
 }
 }

+ 11 - 0
src/__tests__/version.test.ts

@@ -0,0 +1,11 @@
+import { describe, it, expect } from '@jest/globals';
+import { getVersion } from '../util/version.js';
+
+describe('getVersion', () => {
+  it('should return a version string', () => {
+    const version = getVersion();
+    expect(version).toBeDefined();
+    expect(typeof version).toBe('string');
+    expect(version).toMatch(/^\d+\.\d+\.\d+$/);
+  });
+});

+ 3 - 2
src/config/index.ts

@@ -11,6 +11,7 @@ if (!process.env.WIKIJS_URL || !process.env.WIKIJS_API_KEY) {
 }
 }
 
 
 export const CONFIG = {
 export const CONFIG = {
-	WIKIJS_URL: process.env.WIKIJS_URL!,
-	WIKIJS_API_KEY: process.env.WIKIJS_API_KEY!
+	WIKIJS_URL: process.env.WIKIJS_URL,
+	WIKIJS_API_KEY: process.env.WIKIJS_API_KEY,
+	ENABLE_EDIT: process.env.WIKIJS_ENABLE_EDIT === 'true' || false
 };
 };

+ 19 - 0
src/mcp/index.ts

@@ -3,8 +3,11 @@ import { createTool as createSearchPagesTool, PARAMETERS as SEARCH_PAGES_TOOL_PA
 import { createTool as createGetPageByIdTool, PARAMETERS as GET_PAGE_BY_ID_TOOL_PARAMETERS } from './tools/getPageById.js';
 import { createTool as createGetPageByIdTool, PARAMETERS as GET_PAGE_BY_ID_TOOL_PARAMETERS } from './tools/getPageById.js';
 import { createTool as createGetPageByPathTool, PARAMETERS as GET_PAGE_BY_PATH_TOOL_PARAMETERS } from './tools/getPageByPath.js';
 import { createTool as createGetPageByPathTool, PARAMETERS as GET_PAGE_BY_PATH_TOOL_PARAMETERS } from './tools/getPageByPath.js';
 import { createTool as createGetAllPagesTool, PARAMETERS as GET_ALL_PAGES_TOOL_PARAMETERS } from './tools/getAllPages.js';
 import { createTool as createGetAllPagesTool, PARAMETERS as GET_ALL_PAGES_TOOL_PARAMETERS } from './tools/getAllPages.js';
+import { createTool as createEditPageTool, PARAMETERS as EDIT_PAGE_TOOL_PARAMETERS } from './tools/editPage.js';
+import { createTool as createCreatePageTool, PARAMETERS as CREATE_PAGE_TOOL_PARAMETERS } from './tools/createPage.js';
 import { WikiJSClient } from '../wikijs';
 import { WikiJSClient } from '../wikijs';
 import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
 import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
+import { CONFIG } from '../config/index.js';
 
 
 export class WikiJSMcpServer {
 export class WikiJSMcpServer {
     private server: McpServer;
     private server: McpServer;
@@ -51,6 +54,22 @@ export class WikiJSMcpServer {
             GET_ALL_PAGES_TOOL_PARAMETERS,
             GET_ALL_PAGES_TOOL_PARAMETERS,
             createGetAllPagesTool(this.wikiClient)
             createGetAllPagesTool(this.wikiClient)
         );
         );
+
+        if (CONFIG.ENABLE_EDIT) {
+            this.server.tool(
+                'create_page',
+                'Create a new WikiJS page',
+                CREATE_PAGE_TOOL_PARAMETERS,
+                createCreatePageTool(this.wikiClient)
+            );
+
+            this.server.tool(
+                'edit_page',
+                'Edit a WikiJS page by its ID',
+                EDIT_PAGE_TOOL_PARAMETERS,
+                createEditPageTool(this.wikiClient)
+            );
+        }
     }
     }
     
     
     public async connect(transport: Transport) {
     public async connect(transport: Transport) {

+ 59 - 0
src/mcp/tools/createPage.ts

@@ -0,0 +1,59 @@
+import { WikiJSClient } from '../../wikijs/index.js';
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+
+export const PARAMETERS = {
+  title: z.string().describe('The title of the page'),
+  content: z.string().describe('The content of the page'),
+  description: z.string().describe('The description of the page'),
+  path: z.string().describe('The path of the page'),
+  editor: z.string().default('markdown').describe('The editor type (e.g., markdown, visual)'),
+  isPublished: z.boolean().default(true).describe('Whether the page is published'),
+  isPrivate: z.boolean().default(false).describe('Whether the page is private'),
+  locale: z.string().default('en').describe('The locale of the page'),
+  tags: z.array(z.string()).default([]).describe('Tags for the page')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (params) => {
+    const result = await wikiClient.createPage({
+      title: params.title,
+      content: params.content,
+      description: params.description,
+      path: params.path,
+      editor: params.editor,
+      isPublished: params.isPublished,
+      isPrivate: params.isPrivate,
+      locale: params.locale,
+      tags: params.tags
+    });
+
+    if (!result) {
+      return {
+        content: [{
+          type: "text",
+          text: "Failed to create page"
+        }]
+      };
+    }
+
+    if (!result.responseResult?.succeeded) {
+      return {
+        content: [{
+          type: "text",
+          text: `Failed to create page: ${result.responseResult?.message || 'Unknown error'} (Error code: ${result.responseResult?.errorCode})`
+        }]
+      };
+    }
+
+    return {
+      content: [{
+        type: "text",
+        text: JSON.stringify({
+          succeeded: true,
+          message: result.responseResult?.message || 'Page created successfully'
+        }, null, 2)
+      }]
+    };
+  };
+};

+ 50 - 0
src/mcp/tools/editPage.ts

@@ -0,0 +1,50 @@
+import { z } from "zod";
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  id: z.number().describe('The ID of the page to update'),
+  title: z.string().describe('The new title of the page'),
+  content: z.string().describe('The new content of the page'),
+  description: z.string().describe('The new description of the page'),
+  editor: z.string().default('markdown').describe('The editor type (e.g., markdown, visual)'),
+  isPublished: z.boolean().default(true).describe('Whether the page is published'),
+  isPrivate: z.boolean().default(false).describe('Whether the page is private'),
+  locale: z.string().default('en').describe('The locale of the page'),
+  tags: z.array(z.string()).default([]).describe('Tags for the page'),
+  path: z.string().describe('The path of the page')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (params) => {
+    const result = await wikiClient.updatePage(params);
+    
+    if (!result) {
+      return {
+        content: [{
+          type: "text",
+          text: "Failed to update page"
+        }]
+      };
+    }
+
+    if (!result.responseResult?.succeeded) {
+      return {
+        content: [{
+          type: "text",
+          text: `Failed to update page: ${result.responseResult?.message || 'Unknown error'} (Error code: ${result.responseResult?.errorCode})`
+        }]
+      };
+    }
+
+    return {
+      content: [{
+        type: "text",
+        text: JSON.stringify({
+          succeeded: true,
+          message: result.responseResult?.message || 'Page updated successfully'
+        }, null, 2)
+      }]
+    };
+  };
+};

+ 2 - 2
src/util/version.ts

@@ -13,9 +13,9 @@ export function getVersion(): string {
     const __filename = fileURLToPath(import.meta.url);
     const __filename = fileURLToPath(import.meta.url);
     const __dirname = dirname(__filename);
     const __dirname = dirname(__filename);
     const packageJsonPath = join(__dirname, '../../package.json');
     const packageJsonPath = join(__dirname, '../../package.json');
-    const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
+    const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { version: string };
 
 
-    cachedVersion = packageJson.version as string;
+    cachedVersion = packageJson.version;
     return cachedVersion;
     return cachedVersion;
 
 
   } catch (error) {
   } catch (error) {

+ 86 - 0
src/wikijs/generated/graphql.ts

@@ -1851,6 +1851,37 @@ export type SearchPagesQueryVariables = Exact<{
 
 
 export type SearchPagesQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', search: { __typename?: 'PageSearchResponse', suggestions: Array<string | null>, totalHits: number, results: Array<{ __typename?: 'PageSearchResult', title: string, description: string, path: string, locale: string } | null> } } | null };
 export type SearchPagesQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', search: { __typename?: 'PageSearchResponse', suggestions: Array<string | null>, totalHits: number, results: Array<{ __typename?: 'PageSearchResult', title: string, description: string, path: string, locale: string } | null> } } | null };
 
 
+export type CreatePageMutationVariables = Exact<{
+  title: Scalars['String']['input'];
+  content: Scalars['String']['input'];
+  description: Scalars['String']['input'];
+  editor: Scalars['String']['input'];
+  isPublished: Scalars['Boolean']['input'];
+  isPrivate: Scalars['Boolean']['input'];
+  locale: Scalars['String']['input'];
+  tags: Array<InputMaybe<Scalars['String']['input']>> | InputMaybe<Scalars['String']['input']>;
+  path: Scalars['String']['input'];
+}>;
+
+
+export type CreatePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', create?: { __typename?: 'PageResponse', responseResult: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } } | null } | null };
+
+export type UpdatePageMutationVariables = Exact<{
+  id: Scalars['Int']['input'];
+  title: Scalars['String']['input'];
+  content: Scalars['String']['input'];
+  description: Scalars['String']['input'];
+  editor: Scalars['String']['input'];
+  isPublished: Scalars['Boolean']['input'];
+  isPrivate: Scalars['Boolean']['input'];
+  locale: Scalars['String']['input'];
+  tags: Array<InputMaybe<Scalars['String']['input']>> | InputMaybe<Scalars['String']['input']>;
+  path: Scalars['String']['input'];
+}>;
+
+
+export type UpdatePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', update?: { __typename?: 'PageResponse', responseResult: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } } | null } | null };
+
 
 
 export const GetPageByIdDocument = gql`
 export const GetPageByIdDocument = gql`
     query GetPageById($id: Int!) {
     query GetPageById($id: Int!) {
@@ -1922,6 +1953,55 @@ export const SearchPagesDocument = gql`
   }
   }
 }
 }
     `;
     `;
+export const CreatePageDocument = gql`
+    mutation CreatePage($title: String!, $content: String!, $description: String!, $editor: String!, $isPublished: Boolean!, $isPrivate: Boolean!, $locale: String!, $tags: [String]!, $path: String!) {
+  pages {
+    create(
+      title: $title
+      content: $content
+      description: $description
+      editor: $editor
+      isPublished: $isPublished
+      isPrivate: $isPrivate
+      locale: $locale
+      tags: $tags
+      path: $path
+    ) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+    `;
+export const UpdatePageDocument = gql`
+    mutation UpdatePage($id: Int!, $title: String!, $content: String!, $description: String!, $editor: String!, $isPublished: Boolean!, $isPrivate: Boolean!, $locale: String!, $tags: [String]!, $path: String!) {
+  pages {
+    update(
+      id: $id
+      title: $title
+      content: $content
+      description: $description
+      editor: $editor
+      isPublished: $isPublished
+      isPrivate: $isPrivate
+      locale: $locale
+      tags: $tags
+      path: $path
+    ) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+    `;
 
 
 export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string, operationType?: string, variables?: any) => Promise<T>;
 export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string, operationType?: string, variables?: any) => Promise<T>;
 
 
@@ -1941,6 +2021,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
     },
     },
     SearchPages(variables: SearchPagesQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<SearchPagesQuery> {
     SearchPages(variables: SearchPagesQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<SearchPagesQuery> {
       return withWrapper((wrappedRequestHeaders) => client.request<SearchPagesQuery>({ document: SearchPagesDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'SearchPages', 'query', variables);
       return withWrapper((wrappedRequestHeaders) => client.request<SearchPagesQuery>({ document: SearchPagesDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'SearchPages', 'query', variables);
+    },
+    CreatePage(variables: CreatePageMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<CreatePageMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<CreatePageMutation>({ document: CreatePageDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'CreatePage', 'mutation', variables);
+    },
+    UpdatePage(variables: UpdatePageMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<UpdatePageMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<UpdatePageMutation>({ document: UpdatePageDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'UpdatePage', 'mutation', variables);
     }
     }
   };
   };
 }
 }

+ 42 - 0
src/wikijs/index.ts

@@ -38,4 +38,46 @@ export class WikiJSClient {
     const result = await this.sdk.GetPageByPath({ path, locale });
     const result = await this.sdk.GetPageByPath({ path, locale });
     return result.pages?.singleByPath || null;
     return result.pages?.singleByPath || null;
   }
   }
+
+  async createPage(params: {
+    title: string;
+    content: string;
+    description: string;
+    path: string;
+    editor?: string;
+    isPublished?: boolean;
+    isPrivate?: boolean;
+    locale?: string;
+    tags?: string[];
+  }) {
+    const result = await this.sdk.CreatePage({
+      title: params.title,
+      content: params.content,
+      description: params.description,
+      path: params.path,
+      editor: params.editor || 'markdown',
+      isPublished: params.isPublished ?? true,
+      isPrivate: params.isPrivate ?? false,
+      locale: params.locale || 'en',
+      tags: params.tags || []
+    });
+    
+    return result.pages?.create || null;
+  }
+
+  async updatePage(params: {
+    id: number;
+    title: string;
+    content: string;
+    description: string;
+    editor: string;
+    isPublished: boolean;
+    isPrivate: boolean;
+    locale: string;
+    tags: string[];
+    path: string;
+  }) {
+    const result = await this.sdk.UpdatePage(params);
+    return result.pages?.update || null;
+  }
 } 
 } 

+ 68 - 0
src/wikijs/queries/pages.graphql

@@ -62,4 +62,72 @@ query SearchPages($query: String!, $path: String, $locale: String) {
       totalHits
       totalHits
     }
     }
   }
   }
+}
+
+mutation CreatePage(
+  $title: String!
+  $content: String!
+  $description: String!
+  $editor: String!
+  $isPublished: Boolean!
+  $isPrivate: Boolean!
+  $locale: String!
+  $tags: [String]!
+  $path: String!
+) {
+  pages {
+    create(
+      title: $title
+      content: $content
+      description: $description
+      editor: $editor
+      isPublished: $isPublished
+      isPrivate: $isPrivate
+      locale: $locale
+      tags: $tags
+      path: $path
+    ) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+
+mutation UpdatePage(
+  $id: Int!
+  $title: String!
+  $content: String!
+  $description: String!
+  $editor: String!
+  $isPublished: Boolean!
+  $isPrivate: Boolean!
+  $locale: String!
+  $tags: [String]!
+  $path: String!
+) {
+  pages {
+    update(
+      id: $id
+      title: $title
+      content: $content
+      description: $description
+      editor: $editor
+      isPublished: $isPublished
+      isPrivate: $isPrivate
+      locale: $locale
+      tags: $tags
+      path: $path
+    ) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
 }
 }

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff