Переглянути джерело

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 1 рік тому
батько
коміт
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 path and locale
 - 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
 {
   "mcpServers": {
     "wikijs-mcp": {
       "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
 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
 
+### Prerequisites
+- Node.js 18+
+- Docker (for local WikiJS testing)
+- npm or yarn
+
+### Setup
+
 1. Clone the repository:
 ```bash
-git clone https://github.com/RicardoCenci/wikijs-mcp.git
+git clone https://github.com/cahaseler/wikijs-mcp.git
 cd wikijs-mcp
 ```
 
@@ -49,35 +165,73 @@ cd wikijs-mcp
 npm install
 ```
 
-3. Copy the environment template and fill out its contents
+3. Copy the environment template:
 ```bash
 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
+# 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
 ```
 
-5. Deploy the WikiJS instance for testing
+### Testing the MCP Server
 
 ```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
-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
-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_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'],
+};

Різницю між файлами не показано, бо вона завелика
+ 3524 - 1001
package-lock.json


+ 16 - 4
package.json

@@ -8,12 +8,12 @@
 	"license": "MIT",
 	"repository": {
 		"type": "git",
-		"url": "git+https://github.com/RicardoCenci/wikijs-mcp.git"
+		"url": "git+https://github.com/cahaseler/wikijs-mcp.git"
 	},
 	"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"],
 	"files": [
 		"build/**/*",
@@ -25,7 +25,13 @@
 	},
 	"scripts": {
 		"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": {
 		"@modelcontextprotocol/sdk": "^1.12.3",
@@ -40,7 +46,13 @@
 		"@graphql-codegen/typescript": "^4.1.6",
 		"@graphql-codegen/typescript-graphql-request": "^6.3.0",
 		"@graphql-codegen/typescript-operations": "^4.6.1",
+		"@types/jest": "^29.5.0",
 		"@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"
 	}
 }

+ 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 = {
-	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 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 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 { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
+import { CONFIG } from '../config/index.js';
 
 export class WikiJSMcpServer {
     private server: McpServer;
@@ -51,6 +54,22 @@ export class WikiJSMcpServer {
             GET_ALL_PAGES_TOOL_PARAMETERS,
             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) {

+ 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 __dirname = dirname(__filename);
     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;
 
   } 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 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`
     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>;
 
@@ -1941,6 +2021,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
     },
     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);
+    },
+    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 });
     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
     }
   }
+}
+
+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
+      }
+    }
+  }
 }

Деякі файли не було показано, через те що забагато файлів було змінено