#
tokens: 48584/50000 53/64 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/ivo-toby/contentful-mcp?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       ├── pr-check.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .prettierrc
├── .releaserc
├── bin
│   └── mcp-server.js
├── build.js
├── CLAUDE.md
├── codecompanion-workspace.json
├── Dockerfile
├── eslint.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── inspect-watch.js
│   └── inspect.js
├── smithery.yaml
├── src
│   ├── config
│   │   ├── ai-actions-client.ts
│   │   └── client.ts
│   ├── handlers
│   │   ├── ai-action-handlers.ts
│   │   ├── asset-handlers.ts
│   │   ├── bulk-action-handlers.ts
│   │   ├── comment-handlers.ts
│   │   ├── content-type-handlers.ts
│   │   ├── entry-handlers.ts
│   │   └── space-handlers.ts
│   ├── index.ts
│   ├── prompts
│   │   ├── ai-actions-invoke.ts
│   │   ├── ai-actions-overview.ts
│   │   ├── contentful-prompts.ts
│   │   ├── generateVariableTypeContent.ts
│   │   ├── handlePrompt.ts
│   │   ├── handlers.ts
│   │   └── promptHandlers
│   │       ├── aiActions.ts
│   │       └── contentful.ts
│   ├── transports
│   │   ├── sse.ts
│   │   └── streamable-http.ts
│   ├── types
│   │   ├── ai-actions.ts
│   │   └── tools.ts
│   └── utils
│       ├── ai-action-tool-generator.ts
│       ├── summarizer.ts
│       ├── to-camel-case.ts
│       └── validation.ts
├── test
│   ├── integration
│   │   ├── ai-action-handler.test.ts
│   │   ├── ai-actions-client.test.ts
│   │   ├── asset-handler.test.ts
│   │   ├── bulk-action-handler.test.ts
│   │   ├── client.test.ts
│   │   ├── comment-handler.test.ts
│   │   ├── content-type-handler.test.ts
│   │   ├── entry-handler.test.ts
│   │   ├── space-handler.test.ts
│   │   └── streamable-http.test.ts
│   ├── msw-setup.ts
│   ├── setup.ts
│   └── unit
│       ├── ai-action-header.test.ts
│       ├── ai-action-tool-generator.test.ts
│       ├── ai-action-tools.test.ts
│       ├── ai-actions.test.ts
│       ├── content-type-handler-merge.test.ts
│       ├── entry-handler-merge.test.ts
│       └── tools.test.ts
├── tsconfig.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
@contentful:registry=https://registry.npmjs.org

```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "printWidth": 100,
  "semi": false,
  "singleQuote": false
}

```

--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------

```
{
  "branches": ["main", "master"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/npm",
    "@semantic-release/github"
  ]
}

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

build/

gcp-oauth.keys.json
.*-server-credentials.json

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

.DS_Store

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.aider*
Cntfl-Readme.md

**/.claude/settings.local.json

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
<img width="700" src="https://images.ctfassets.net/jtqsy5pye0zd/6wNuQ2xMvbw134rccObi0q/bf61badc6d6d9780609e541713f0bba6/Contentful_Logo_2.5_Dark.svg?w=700&q=100" alt="Contentful MCP server"/>

# Contentful MCP Server

## Notice

This is a community driven server! Contentful has released an official server which you can find [here](https://github.com/contentful/contentful-mcp-server)

[![smithery badge](https://smithery.ai/badge/@ivotoby/contentful-management-mcp-server)](https://smithery.ai/server/@ivotoby/contentful-management-mcp-server)

An MCP server implementation that integrates with Contentful's Content Management API, providing comprehensive content management capabilities.

- Please note \*; if you are not interested in the code, and just want to use this MCP in
  Claude Desktop (or any other tool that is able to use MCP servers) you don't have to
  clone this repo, you can just set it up in Claude desktop, refer to the section
  "Usage with Claude Desktop" for instructions on how to install it.

<a href="https://glama.ai/mcp/servers/l2fxeaot4p"><img width="380" height="200" src="https://glama.ai/mcp/servers/l2fxeaot4p/badge" alt="contentful-mcp MCP server" /></a>

## Features

- **Content Management**: Full CRUD operations for entries and assets
- **Comment Management**: Create, retrieve, and manage comments on entries with support for both plain-text and rich-text formats, including threaded conversations
- **Space Management**: Create, update, and manage spaces and environments
- **Content Types**: Manage content type definitions
- **Localization**: Support for multiple locales
- **Publishing**: Control content publishing workflow
- **Bulk Operations**: Execute bulk publishing, unpublishing, and validation across multiple entries and assets
- **Smart Pagination**: List operations return maximum 3 items per request to prevent context window overflow, with built-in pagination support

## Pagination

To prevent context window overflow in LLMs, list operations (like search_entries and list_assets) are limited to 3 items per request. Each response includes:

- Total number of available items
- Current page of items (max 3)
- Number of remaining items
- Skip value for the next page
- Message prompting the LLM to offer retrieving more items

This pagination system allows the LLM to efficiently handle large datasets while maintaining context window limits.

## Bulk Operations

The bulk operations feature provides efficient management of multiple content items simultaneously:

- **Asynchronous Processing**: Operations run asynchronously and provide status updates
- **Efficient Content Management**: Process multiple entries or assets in a single API call
- **Status Tracking**: Monitor progress with success and failure counts
- **Resource Optimization**: Reduce API calls and improve performance for batch operations

These bulk operation tools are ideal for content migrations, mass updates, or batch publishing workflows.

## Tools

### Entry Management

- **search_entries**: Search for entries using query parameters
- **create_entry**: Create new entries
- **get_entry**: Retrieve existing entries
- **update_entry**: Update entry fields
- **delete_entry**: Remove entries
- **publish_entry**: Publish entries
- **unpublish_entry**: Unpublish entries

### Comment Management

- **get_comments**: Retrieve comments for an entry with filtering by status (active, resolved, all)
- **create_comment**: Create new comments on entries with support for both plain-text and rich-text formats. Supports threaded conversations by providing a parent comment ID to reply to existing comments
- **get_single_comment**: Retrieve a specific comment by its ID for an entry
- **delete_comment**: Delete a specific comment from an entry
- **update_comment**: Update existing comments with new body content or status changes

#### Threaded Comments

Comments support threading functionality to enable structured conversations and work around the 512-character limit:

- **Reply to Comments**: Use the `parent` parameter in `create_comment` to reply to an existing comment
- **Threaded Conversations**: Build conversation trees by replying to specific comments
- **Extended Discussions**: Work around the 512-character limit by creating threaded replies to continue longer messages
- **Conversation Context**: Maintain context in discussions by organizing related comments in threads

Example usage:

1. Create a main comment: `create_comment` with `entryId`, `body`, and `status`
2. Reply to that comment: `create_comment` with `entryId`, `body`, `status`, and `parent` (the ID of the comment you're replying to)
3. Continue the thread: Reply to any comment in the thread by using its ID as the `parent`

### Bulk Operations

- **bulk_publish**: Publish multiple entries and assets in a single operation. Accepts an array of entities (entries and assets) and processes their publication as a batch.
- **bulk_unpublish**: Unpublish multiple entries and assets in a single operation. Similar to bulk_publish but removes content from the delivery API.
- **bulk_validate**: Validate multiple entries for content consistency, references, and required fields. Returns validation results without modifying content.

### Asset Management

- **list_assets**: List assets with pagination (3 items per page)
- **upload_asset**: Upload new assets with metadata
- **get_asset**: Retrieve asset details and information
- **update_asset**: Update asset metadata and files
- **delete_asset**: Remove assets from space
- **publish_asset**: Publish assets to delivery API
- **unpublish_asset**: Unpublish assets from delivery API

### Space & Environment Management

- **list_spaces**: List available spaces
- **get_space**: Get space details
- **list_environments**: List environments in a space
- **create_environment**: Create new environment
- **delete_environment**: Remove environment

### Content Type Management

- **list_content_types**: List available content types
- **get_content_type**: Get content type details
- **create_content_type**: Create new content type
- **update_content_type**: Update content type
- **delete_content_type**: Remove content type
- **publish_content_type**: Publish a content type

## Development Tools

### MCP Inspector

The project includes an MCP Inspector tool that helps with development and debugging:

- **Inspect Mode**: Run `npm run inspect` to start the inspector, you can open the inspector by going to http://localhost:5173
- **Watch Mode**: Use `npm run inspect:watch` to automatically restart the inspector when files change
- **Visual Interface**: The inspector provides a web interface to test and debug MCP tools
- **Real-time Testing**: Try out tools and see their responses immediately
- **Bulk Operations Testing**: Test and monitor bulk operations with visual feedback on progress and results

The project also contains a `npm run dev` command which rebuilds and reloads the MCP server on every change.

## Configuration

### Prerequisites

1. Create a Contentful account at [Contentful](https://www.contentful.com/)
2. Generate a Content Management API token from your account settings

### Environment Variables

These variables can also be set as arguments

- `CONTENTFUL_HOST` / `--host`: Contentful Management API Endpoint (defaults to https://api.contentful.com)
- `CONTENTFUL_MANAGEMENT_ACCESS_TOKEN` / `--management-token`: Your Content Management API token
- `ENABLE_HTTP_SERVER` / `--http`: Set to "true" to enable HTTP/SSE mode
- `HTTP_PORT` / `--port`: Port for HTTP server (default: 3000)
- `HTTP_HOST` / `--http-host`: Host for HTTP server (default: localhost)

### Space and Environment Scoping

You can scope the spaceId and EnvironmentId to ensure the LLM will only do operations on the defined space/env ID's.
This is mainly to support agents that are to operate within specific spaces. If both `SPACE_ID` and `ENVIRONMENT_ID` env-vars are set
the tools will not report needing these values and the handlers will use the environment vars to do CMA operations.
You will also loose access to the tools in the space-handler, since these tools are across spaces.
You can also add the `SPACE_ID` and `ENVIRONMENT_ID` by using arguments `--space-id` and `--environment-id`

#### Using App Identity

Instead of providing a Management token you can also leverage [App Identity](https://www.contentful.com/developers/docs/extensibility/app-framework/app-identity/)
for handling authentication. You would have to setup and install a Contentful App and set the following parameters when calling the MCP-server:

- `--app-id` = the app Id which is providing the Apptoken
- `--private-key` = the private key you created in the user-interface with your app, tied to `app_id`
- `--space-id` = the spaceId in which the app is installed
- `--environment-id` = the environmentId (within the space) in which the app is installed.

With these values the MCP server will request a temporary AppToken to do content operation in the defined space/environment-id. This especially useful when using this MCP server in backend systems that act as MCP-client (like chat-agents)

### Usage with Claude Desktop

You do not need to clone this repo to use this MCP, you can simply add it to
your `claude_desktop_config.json`:

Add or edit `~/Library/Application Support/Claude/claude_desktop_config.json`
and add the following lines:

```json
{
  "mcpServers": {
    "contentful": {
      "command": "npx",
      "args": ["-y", "@ivotoby/contentful-management-mcp-server"],
      "env": {
        "CONTENTFUL_MANAGEMENT_ACCESS_TOKEN": "<Your CMA token>"
      }
    }
  }
}
```

If your MCPClient does not support setting environment variables you can also set the management token using an argument like this:

```json
{
  "mcpServers": {
    "contentful": {
      "command": "npx",
      "args": [
        "-y",
        "@ivotoby/contentful-management-mcp-server",
        "--management-token",
        "<your token>",
        "--host",
        "http://api.contentful.com"
      ]
    }
  }
}
```

### Installing via Smithery

To install Contentful Management Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@ivotoby/contentful-management-mcp-server):

```bash
npx -y @smithery/cli install @ivotoby/contentful-management-mcp-server --client claude
```

### Developing and using Claude desktop

If you want to contribute and test what Claude does with your contributions;

- run `npm run dev`, this will start the watcher that rebuilds the MCP server on every change
- update `claude_desktop_config.json` to reference the project directly, ie;

```
{
  "mcpServers": {
    "contentful": {
      "command": "node",
      "args": ["/Users/ivo/workspace/contentful-mcp/bin/mcp-server.js"],
      "env": {
        "CONTENTFUL_MANAGEMENT_ACCESS_TOKEN": "<Your CMA Token>"
      }
    }
  }
}
```

This will allow you to test any modification in the MCP server with Claude directly, however; if you add new tools/resources you will need to restart Claude Desktop

## Transport Modes

The MCP server supports two transport modes:

### stdio Transport

The default transport mode uses standard input/output streams for communication. This is ideal for integration with MCP clients that support stdio transport, like Claude Desktop.

To use stdio mode, simply run the server without the `--http` flag:

```bash
npx -y contentful-mcp --management-token YOUR_TOKEN
# or alternatively
npx -y @ivotoby/contentful-management-mcp-server --management-token YOUR_TOKEN
```

### StreamableHTTP Transport

The server also supports the StreamableHTTP transport as defined in the MCP protocol. This mode is useful for web-based integrations or when running the server as a standalone service.

To use StreamableHTTP mode, run with the `--http` flag:

```bash
npx -y contentful-mcp --management-token YOUR_TOKEN --http --port 3000
# or alternatively
npx -y @ivotoby/contentful-management-mcp-server --management-token YOUR_TOKEN --http --port 3000
```

#### StreamableHTTP Details

- Uses the official MCP StreamableHTTP transport
- Supports standard MCP protocol operations
- Includes session management for maintaining state
- Properly handles initialize/notify patterns
- Compatible with standard MCP clients
- Replaces the deprecated SSE transport with the modern approach

The implementation follows the standard MCP protocol specification, allowing any MCP client to connect to the server without special handling.

## Error Handling

The server implements comprehensive error handling for:

- Authentication failures
- Rate limiting
- Invalid requests
- Network issues
- API-specific errors

[![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/146d4235-bdb1-492e-b594-82fd27b77388)

## License

MIT License

## Fine print

This MCP Server enables Claude (or other agents that can consume MCP resources) to update, delete content, spaces and content-models. So be sure what you allow Claude to do with your Contentful spaces!

This MCP-server is not officially supported by Contentful (yet)

```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
# Contentful MCP - Development Guide

## Common Commands
- Build: `npm run build`
- Type Check: `npm run typecheck`
- Lint: `npm run lint`
- Run Tests: `npm test`
- Run Single Test: `npx vitest run test/path/to/test.test.ts`
- Run Tests in Watch Mode: `npm run test:watch`
- Dev Mode (watch & rebuild): `npm run dev`

## Code Style Guidelines
- **Formatting**: Uses Prettier with 100 char width, no semicolons, double quotes
- **TypeScript**: Use strict typing, avoid `any` when possible
- **Imports**: Order from external to internal, group related imports
- **Naming**: Use camelCase for variables/functions, PascalCase for types/interfaces
- **Error Handling**: Always handle errors in async functions with try/catch blocks
- **Documentation**: Add JSDoc style comments for functions and interfaces

## Entity Structure
- Tools and handlers are organized by entity type (Entry, Asset, Content Type, etc.)
- Each handler should focus on a single responsibility
- Bulk actions should use the Contentful API's bulk operation endpoints

## Testing
Tests use Vitest with MSW for API mocking. Organize tests in:
- `test/unit/` - Unit tests for utility functions
- `test/integration/` - Tests that verify handler behavior
```

--------------------------------------------------------------------------------
/src/prompts/handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { handlePrompt } from "./handlePrompt";

// Re-export the handlePrompt function as the main export
export { handlePrompt };
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
// @ts-check

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  tseslint.configs.recommended,
);

```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['test/**/*.test.ts'],
    setupFiles: ['test/setup.ts']
  }
})

```

--------------------------------------------------------------------------------
/src/utils/to-camel-case.ts:
--------------------------------------------------------------------------------

```typescript
export const toCamelCase = (str: string): string =>
  str
    .split(/\s+/)
    .map((word: string, index: number) =>
      index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
    )
    .join("")

```

--------------------------------------------------------------------------------
/build.js:
--------------------------------------------------------------------------------

```javascript
import * as esbuild from "esbuild";
await esbuild.build({
  entryPoints: ["./src/index.ts"],
  bundle: true,
  platform: "node",
  format: "esm",
  outfile: "./dist/bundle.js",
  target: "node18",
  banner: {
    js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);`,
  },
});

```

--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------

```typescript
import { beforeAll, expect } from 'vitest';
import dotenv from "dotenv";

// Load environment variables from .env file
dotenv.config();

// Make sure we have the required environment variables
beforeAll(() => {
  const requiredEnvVars = ["CONTENTFUL_MANAGEMENT_ACCESS_TOKEN"];

  for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
      throw new Error(`Missing required environment variable: ${envVar}`);
    }
  }
});

export { expect };

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": ".",
    "target": "ES2022",
    "module": "ES2020",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "sourceMap": false,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "allowImportingTsExtensions": true,
    "noEmit": true
  },
  "include": [
    "src/**/*",
    "test/**/*",
    "examples/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

```

--------------------------------------------------------------------------------
/.github/workflows/pr-check.yml:
--------------------------------------------------------------------------------

```yaml
name: PR Check

on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches: 
      - main
      - master

jobs:
  verify:
    name: Verify PR
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "lts/*"
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        env:
          CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN }}
        run: npm test

```

--------------------------------------------------------------------------------
/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:
      - contentfulManagementAccessToken
    properties:
      contentfulManagementAccessToken:
        type: string
        description: Your Content Management API token from Contentful
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => ({ command: 'node', args: ['bin/mcp-server.js'], env: { CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: config.contentfulManagementAccessToken } })
```

--------------------------------------------------------------------------------
/scripts/inspect.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

import { fileURLToPath } from "url";
import { dirname, resolve } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const serverPath = resolve(__dirname, "../bin/mcp-server.js");

const args = ["npx", "@modelcontextprotocol/inspector", "node", serverPath];

// Add environment variables as CLI arguments if they exist
if (process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN) {
  args.push(`--headers=${process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN}`);
}

// Execute the command
import { spawn } from "child_process";
const inspect = spawn(args[0], args.slice(1), { stdio: "inherit" });

inspect.on("error", (err) => {
  console.error("Failed to start inspector:", err);
  process.exit(1);
});

inspect.on("exit", (code) => {
  process.exit(code || 0);
});

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
name: Release
on:
  push:
    branches:
      - main
      - master

permissions:
  contents: write
  issues: write
  pull-requests: write

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "lts/*"
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        env:
          CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN }}
        run: npm test

      - name: Build
        run: npm run build

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release

```

--------------------------------------------------------------------------------
/src/prompts/handlePrompt.ts:
--------------------------------------------------------------------------------

```typescript
import { GetPromptResult } from "@modelcontextprotocol/sdk/types";
import { contentfulHandlers } from "./promptHandlers/contentful";
import { aiActionsHandlers } from "./promptHandlers/aiActions";

/**
 * Handle a prompt request and return the appropriate response
 * @param name Prompt name
 * @param args Optional arguments provided for the prompt
 * @returns Prompt result with messages
 */
export async function handlePrompt(
  name: string,
  args?: Record<string, string>,
): Promise<GetPromptResult> {
  // Check for AI Actions handlers
  if (name.startsWith("ai-actions-") && name in aiActionsHandlers) {
    return aiActionsHandlers[name as keyof typeof aiActionsHandlers](args);
  }
  
  // Check for general Contentful handlers
  if (name in contentfulHandlers) {
    return contentfulHandlers[name as keyof typeof contentfulHandlers](args);
  }
  
  // Handle unknown prompts
  throw new Error(`Unknown prompt: ${name}`);
}
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use an official Node.js image as the base image
FROM node:22-alpine AS builder

# Set the working directory
WORKDIR /app

# Copy package files and source code
COPY . .

# Install dependencies
RUN --mount=type=cache,target=/root/.npm npm install

# Build the application
RUN npm run build

# Use a smaller Node.js image for the runtime
FROM node:22-alpine AS runtime

# Set the working directory
WORKDIR /app

# Copy built files from the builder stage
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/bin /app/bin
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/package.json /app/package.json

# Environment variable for Contentful Management API token
ENV CONTENTFUL_MANAGEMENT_ACCESS_TOKEN=your_contentful_management_api_token

# Expose any required ports (if needed by the application)
# EXPOSE 3000

# Start the server
ENTRYPOINT ["node", "bin/mcp-server.js"]
```

--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------

```typescript
export function validateEnvironment(): void {
  const {
    CONTENTFUL_MANAGEMENT_ACCESS_TOKEN,
    PRIVATE_KEY,
    APP_ID,
    SPACE_ID,
    ENVIRONMENT_ID,
    ENABLE_HTTP_SERVER,
    HTTP_PORT
  } = process.env

  if (!CONTENTFUL_MANAGEMENT_ACCESS_TOKEN && !PRIVATE_KEY) {
    console.error("Either CONTENTFUL_MANAGEMENT_ACCESS_TOKEN or PRIVATE_KEY must be set")
    process.exit(1)
  }

  if (PRIVATE_KEY) {
    if (!APP_ID) {
      console.error("APP_ID is required when using PRIVATE_KEY")
      process.exit(1)
    }
    if (!SPACE_ID) {
      console.error("SPACE_ID is required when using PRIVATE_KEY")
      process.exit(1)
    }
    if (!ENVIRONMENT_ID) {
      console.error("ENVIRONMENT_ID is required when using PRIVATE_KEY")
      process.exit(1)
    }
  }

  // Validate HTTP server settings if enabled
  if (ENABLE_HTTP_SERVER === "true") {
    if (HTTP_PORT) {
      const port = parseInt(HTTP_PORT)
      if (isNaN(port) || port < 1 || port > 65535) {
        console.error("HTTP_PORT must be a valid port number (1-65535)")
        process.exit(1)
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/utils/summarizer.ts:
--------------------------------------------------------------------------------

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */

export interface SummarizeOptions {
  maxItems?: number
  indent?: number
  showTotal?: boolean
  remainingMessage?: string
}

export const summarizeData = (data: any, options: SummarizeOptions = {}): any => {
  const { maxItems = 3, remainingMessage = "To see more items, please ask me to retrieve the next page." } =
    options

  // Handle Contentful-style responses with items and total
  if (data && typeof data === "object" && "items" in data && "total" in data) {
    const items = data.items
    const total = data.total

    if (items.length <= maxItems) {
      return data
    }

    return {
      items: items.slice(0, maxItems),
      total: total,
      showing: maxItems,
      remaining: total - maxItems,
      message: remainingMessage,
      skip: maxItems // Add skip value for next page
    }
  }

  // Handle plain arrays
  if (Array.isArray(data)) {
    if (data.length <= maxItems) {
      return data
    }

    return {
      items: data.slice(0, maxItems),
      total: data.length,
      showing: maxItems,
      remaining: data.length - maxItems,
      message: remainingMessage,
      skip: maxItems // Add skip value for next page
    }
  }

  // Return non-array data as-is
  return data
}

```

--------------------------------------------------------------------------------
/src/config/client.ts:
--------------------------------------------------------------------------------

```typescript
import { getManagementToken } from "@contentful/node-apps-toolkit"
import { createClient } from "contentful-management"

const {
  CONTENTFUL_MANAGEMENT_ACCESS_TOKEN,
  CONTENTFUL_HOST = "api.contentful.com",
  PRIVATE_KEY,
  APP_ID,
  SPACE_ID,
  ENVIRONMENT_ID,
} = process.env

export const getContentfulClient = async () => {
  let formattedKey = ""
  if (!CONTENTFUL_MANAGEMENT_ACCESS_TOKEN && !PRIVATE_KEY) {
    throw new Error("No Contentful management token or private key found...")
  }
  if (PRIVATE_KEY) {
    const formatKey = (key: string) => {
      // Remove existing headers, spaces, and line breaks
      const cleanKey = key
        .replace("-----BEGIN RSA PRIVATE KEY-----", "")
        .replace("-----END RSA PRIVATE KEY-----", "")
        .replace(/\s/g, "")

      // Split into 64-character lines
      const chunks = cleanKey.match(/.{1,64}/g) || []

      // Reassemble with proper format
      return ["-----BEGIN RSA PRIVATE KEY-----", ...chunks, "-----END RSA PRIVATE KEY-----"].join(
        "\n",
      )
    }

    formattedKey = formatKey(PRIVATE_KEY!)
  }

  const accessToken =
    CONTENTFUL_MANAGEMENT_ACCESS_TOKEN ||
    (await getManagementToken(formattedKey!, {
      appInstallationId: APP_ID!,
      spaceId: SPACE_ID!,
      environmentId: ENVIRONMENT_ID!,
      host: "https://" + CONTENTFUL_HOST,
    }))

  return createClient(
    {
      accessToken,
      host: CONTENTFUL_HOST,
      headers: {
        "X-Contentful-user-agent": "contentful-community-mcp/1.0.0",
      },
    },
    { type: "plain" },
  )
}

```

--------------------------------------------------------------------------------
/test/unit/ai-action-header.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest"
import fs from 'fs'
import path from 'path'

// We'll test the implementation by directly examining the source code
// since mocking the Contentful client has been causing issues
describe("AI Actions Alpha Header", () => {
  it("should have alpha header implementation in ai-actions-client.ts", () => {
    // Read the ai-actions-client.ts file
    const clientPath = path.join(__dirname, '../../src/config/ai-actions-client.ts')
    const fileContent = fs.readFileSync(clientPath, 'utf8')
    
    // Check for alpha header constants
    expect(fileContent).toContain('X-Contentful-Enable-Alpha-Feature')
    expect(fileContent).toContain('ai-service')
    
    // Check for withAlphaHeader function
    expect(fileContent).toContain('function withAlphaHeader')
    
    // Check that the function is used in all API calls
    const apiCalls = [
      'client\\.raw\\.get\\(',
      'client\\.raw\\.post\\(',
      'client\\.raw\\.put\\(',
      'client\\.raw\\.delete\\('
    ]
    
    // Each API call should be followed by withAlphaHeader or include it as a parameter
    apiCalls.forEach(call => {
      // Count occurrences of the API call
      const callMatches = fileContent.match(new RegExp(call, 'g')) || []
      
      // Count occurrences of withAlphaHeader near API calls
      const withHeaderPattern = new RegExp(`${call}[^]*?withAlphaHeader`, 'g')
      const withHeaderMatches = fileContent.match(withHeaderPattern) || []
      
      // Every API call should have a corresponding withAlphaHeader
      expect(withHeaderMatches.length).toBeGreaterThan(0)
      
      // This isn't a strict test but checks that we're using the pattern broadly
      console.log(`${call} occurrences: ${callMatches.length}, with header: ${withHeaderMatches.length}`)
    })
  })
})
```

--------------------------------------------------------------------------------
/src/handlers/space-handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { getContentfulClient } from "../config/client.js"

export const spaceHandlers = {
  listSpaces: async () => {
    const contentfulClient = await getContentfulClient()
    const spaces = await contentfulClient.space.getMany({})
    return {
      content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }],
    }
  },

  getSpace: async (args: { spaceId: string }) => {
    const spaceId = args.spaceId
    if (!spaceId) {
      throw new Error("spaceId is required.")
    }

    const contentfulClient = await getContentfulClient()
    const space = await contentfulClient.space.get({ spaceId })
    return {
      content: [{ type: "text", text: JSON.stringify(space, null, 2) }],
    }
  },

  listEnvironments: async (args: { spaceId: string }) => {
    const contentfulClient = await getContentfulClient()
    const environments = await contentfulClient.environment.getMany({
      spaceId: args.spaceId,
    })
    return {
      content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
    }
  },

  createEnvironment: async (args: { spaceId: string; environmentId: string; name: string }) => {
    const contentfulClient = await getContentfulClient()
    const environment = await contentfulClient.environment.create(
      {
        spaceId: args.spaceId,
        environmentId: args.environmentId,
      },
      {
        name: args.name,
      },
    )
    return {
      content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
    }
  },

  deleteEnvironment: async (args: { spaceId: string; environmentId: string }) => {
    const contentfulClient = await getContentfulClient()
    await contentfulClient.environment.delete({
      spaceId: args.spaceId,
      environmentId: args.environmentId,
    })
    return {
      content: [
        {
          type: "text",
          text: `Environment ${args.environmentId} deleted successfully`,
        },
      ],
    }
  },
}

```

--------------------------------------------------------------------------------
/bin/mcp-server.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node
/* eslint-disable no-undef */

async function main() {
  // Find the management token argument
  const tokenIndex = process.argv.findIndex((arg) => arg === "--management-token")
  if (tokenIndex !== -1 && process.argv[tokenIndex + 1]) {
    process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = process.argv[tokenIndex + 1]
  }

  const hostIndex = process.argv.findIndex((arg) => arg === "--host")
  if (hostIndex !== -1 && process.argv[hostIndex + 1]) {
    process.env.CONTENTFUL_HOST = process.argv[hostIndex + 1]
  }

  const envIdIndex = process.argv.findIndex((arg) => arg === "--environment-id")
  if (envIdIndex !== -1 && process.argv[envIdIndex + 1]) {
    process.env.ENVIRONMENT_ID = process.argv[envIdIndex + 1]
  }

  const spaceIdIndex = process.argv.findIndex((arg) => arg === "--space-id")
  if (spaceIdIndex !== -1 && process.argv[spaceIdIndex + 1]) {
    process.env.SPACE_ID = process.argv[spaceIdIndex + 1]
  }

  const keyIdIndex = process.argv.findIndex((arg) => arg === "--private-key")
  if (keyIdIndex !== -1 && process.argv[keyIdIndex + 1]) {
    process.env.PRIVATE_KEY = process.argv[keyIdIndex + 1]
  }

  const appIdIndex = process.argv.findIndex((arg) => arg === "--app-id")
  if (appIdIndex !== -1 && process.argv[appIdIndex + 1]) {
    process.env.APP_ID = process.argv[appIdIndex + 1]
  }

  // Check for HTTP server mode flag
  const httpServerFlagIndex = process.argv.findIndex((arg) => arg === "--http")
  if (httpServerFlagIndex !== -1) {
    process.env.ENABLE_HTTP_SERVER = "true"

    // Check for HTTP port
    const httpPortIndex = process.argv.findIndex((arg) => arg === "--port")
    if (httpPortIndex !== -1 && process.argv[httpPortIndex + 1]) {
      process.env.HTTP_PORT = process.argv[httpPortIndex + 1]
    }

    // Check for HTTP host
    const httpHostIndex = process.argv.findIndex((arg) => arg === "--http-host")
    if (httpHostIndex !== -1 && process.argv[httpHostIndex + 1]) {
      process.env.HTTP_HOST = process.argv[httpHostIndex + 1]
    }
  }

  // Import and run the bundled server after env var is set
  await import("../dist/bundle.js")
}

main().catch((error) => {
  console.error("Failed to start server:", error)
  process.exit(1)
})

```

--------------------------------------------------------------------------------
/scripts/inspect-watch.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

import { spawn } from 'child_process';
import nodemon from 'nodemon';
import { exec } from 'child_process';

let currentInspector = null;
let isShuttingDown = false;

// Function to kill all node processes running the inspector
function killAllInspectors() {
  return new Promise((resolve) => {
    if (process.platform === 'win32') {
      exec('taskkill /F /IM node.exe /FI "WINDOWTITLE eq @modelcontextprotocol/inspector*"');
    } else {
      exec('pkill -f "@modelcontextprotocol/inspector"');
    }
    resolve();
  });
}

// Function to run the inspector
function startInspector() {
  if (isShuttingDown) return null;
  
  const inspector = spawn('npm', ['run', 'inspect'], {
    stdio: 'inherit',
    shell: true
  });

  inspector.on('error', (err) => {
    console.error('Inspector failed to start:', err);
  });

  return inspector;
}

// Cleanup function
async function cleanup() {
  isShuttingDown = true;
  
  if (currentInspector) {
    currentInspector.kill('SIGTERM');
    currentInspector = null;
  }
  
  await killAllInspectors();
  nodemon.emit('quit');
}

// Set up nodemon to watch the src directory
nodemon({
  watch: ['src'],
  ext: 'ts',
  exec: 'npm run build'
});

// Handle nodemon events
nodemon
  .on('start', () => {
    console.log('Starting build...');
  })
  .on('restart', async () => {
    console.log('Files changed, rebuilding...');
    if (currentInspector) {
      currentInspector.kill('SIGTERM');
      await killAllInspectors();
    }
  })
  .on('quit', () => {
    console.log('Nodemon stopped');
    cleanup().then(() => process.exit(0));
  })
  .on('error', (err) => {
    console.error('Nodemon error:', err);
  })
  .on('crash', () => {
    console.error('Application crashed');
    cleanup();
  })
  .on('exit', () => {
    if (!isShuttingDown) {
      if (currentInspector) {
        currentInspector.kill('SIGTERM');
      }
      currentInspector = startInspector();
    }
  });

// Handle process termination
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGHUP', cleanup);

// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err);
  cleanup().then(() => process.exit(1));
});

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@ivotoby/contentful-management-mcp-server",
  "version": "1.14.0",
  "description": "MCP server for Contentful Content Management API integration",
  "license": "MIT",
  "type": "module",
  "main": "./dist/bundle.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ivo-toby/contentful-mcp.git"
  },
  "bin": {
    "mcp-server-contentful": "./bin/mcp-server.js",
    "contentful-mcp": "./bin/mcp-server.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "node build.js && chmod +x bin/mcp-server.js",
    "clean": "rm -rf dist",
    "lint": "eslint src/**/*.ts",
    "watch": "tsc --watch",
    "dev": "nodemon --watch src -e ts --exec 'npm run build'",
    "typecheck": "tsc --noEmit",
    "prepare": "npm run build",
    "inspect": "node -r dotenv/config ./scripts/inspect.js",
    "inspect-watch": "node ./scripts/inspect-watch.js",
    "test": "vitest run --config vitest.config.ts",
    "test:watch": "vitest watch --config vitest.config.ts"
  },
  "dependencies": {
    "@contentful/node-apps-toolkit": "^3.13.0",
    "@modelcontextprotocol/sdk": "1.11.1",
    "contentful-management": "^11.52.2",
    "cors": "^2.8.5",
    "dotenv": "^16.5.0",
    "express": "^4.18.3",
    "zod": "^3.24.4",
    "zod-to-json-schema": "^3.24.5"
  },
  "devDependencies": {
    "@eslint/js": "^9.19.0",
    "@semantic-release/commit-analyzer": "^11.1.0",
    "@semantic-release/github": "^9.2.6",
    "@semantic-release/npm": "^11.0.3",
    "@semantic-release/release-notes-generator": "^12.1.0",
    "@types/chai": "^4.3.11",
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/mocha": "^10.0.6",
    "@types/node": "^20.10.0",
    "@types/sinon": "^17.0.3",
    "@types/supertest": "^6.0.2",
    "@typescript-eslint/eslint-plugin": "^6.12.0",
    "@typescript-eslint/parser": "^6.12.0",
    "chai": "^5.0.0",
    "esbuild": "^0.19.9",
    "eslint": "^8.57.1",
    "eslint-plugin-perfectionist": "^4.7.0",
    "mocha": "^10.2.0",
    "msw": "^2.7.0",
    "nodemon": "^3.1.9",
    "prettier": "^3.4.2",
    "semantic-release": "^22.0.12",
    "sinon": "^17.0.1",
    "supertest": "^6.3.3",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2",
    "typescript-eslint": "^8.22.0",
    "vitest": "^3.1.3"
  }
}

```

--------------------------------------------------------------------------------
/test/unit/tools.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from "vitest"
import { getSpaceEnvProperties } from "../../src/types/tools"

describe("getSpaceEnvProperties", () => {
  const originalEnv = process.env

  beforeEach(() => {
    process.env = { ...originalEnv }
  })

  afterEach(() => {
    process.env = originalEnv
  })

  it("should add spaceId and environmentId properties when environment variables are not set", () => {
    delete process.env.SPACE_ID
    delete process.env.ENVIRONMENT_ID

    const config = {
      type: "object",
      properties: {
        existingProperty: { type: "string" },
      },
      required: ["existingProperty"],
    }

    const result = getSpaceEnvProperties(config)

    expect(result.properties).toHaveProperty("spaceId")
    expect(result.properties).toHaveProperty("environmentId")
    expect(result.required).toContain("spaceId")
    expect(result.required).toContain("environmentId")
  })

  it("should not add spaceId and environmentId properties when environment variables are set", () => {
    process.env.SPACE_ID = "test-space-id"
    process.env.ENVIRONMENT_ID = "test-environment-id"

    const config = {
      type: "object",
      properties: {
        existingProperty: { type: "string" },
      },
      required: ["existingProperty"],
    }

    const result = getSpaceEnvProperties(config)

    expect(result.properties).not.toHaveProperty("spaceId")
    expect(result.properties).not.toHaveProperty("environmentId")
    expect(result.required).not.toContain("spaceId")
    expect(result.required).not.toContain("environmentId")
  })

  it("should merge spaceId and environmentId properties with existing properties", () => {
    delete process.env.SPACE_ID
    delete process.env.ENVIRONMENT_ID

    const config = {
      type: "object",
      properties: {
        existingProperty: { type: "string" },
      },
      required: ["existingProperty"],
    }

    const result = getSpaceEnvProperties(config)

    expect(result.properties).toHaveProperty("existingProperty")
    expect(result.properties).toHaveProperty("spaceId")
    expect(result.properties).toHaveProperty("environmentId")
    expect(result.required).toContain("existingProperty")
    expect(result.required).toContain("spaceId")
    expect(result.required).toContain("environmentId")
  })
})

```

--------------------------------------------------------------------------------
/test/unit/ai-action-tools.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest"
import { getAiActionTools } from "../../src/types/tools"

describe("AI Action Tool Definitions", () => {
  const tools = getAiActionTools()
  
  it("should export the correct AI Action tools", () => {
    expect(tools).toHaveProperty("LIST_AI_ACTIONS")
    expect(tools).toHaveProperty("GET_AI_ACTION")
    expect(tools).toHaveProperty("CREATE_AI_ACTION")
    expect(tools).toHaveProperty("UPDATE_AI_ACTION")
    expect(tools).toHaveProperty("DELETE_AI_ACTION")
    expect(tools).toHaveProperty("PUBLISH_AI_ACTION")
    expect(tools).toHaveProperty("UNPUBLISH_AI_ACTION")
    expect(tools).toHaveProperty("INVOKE_AI_ACTION")
    expect(tools).toHaveProperty("GET_AI_ACTION_INVOCATION")
  })
  
  it("should have the correct schema for LIST_AI_ACTIONS", () => {
    const tool = tools.LIST_AI_ACTIONS
    
    expect(tool.name).toBe("list_ai_actions")
    expect(tool.description).toContain("List all AI Actions")
    
    const schema = tool.inputSchema
    expect(schema.properties).toHaveProperty("limit")
    expect(schema.properties).toHaveProperty("skip")
    expect(schema.properties).toHaveProperty("status")
  })
  
  it("should have the correct schema for INVOKE_AI_ACTION", () => {
    const tool = tools.INVOKE_AI_ACTION
    
    expect(tool.name).toBe("invoke_ai_action")
    expect(tool.description).toContain("Invoke an AI Action")
    
    const schema = tool.inputSchema
    expect(schema.properties).toHaveProperty("aiActionId")
    expect(schema.properties).toHaveProperty("variables")
    expect(schema.properties).toHaveProperty("rawVariables")
    expect(schema.properties).toHaveProperty("outputFormat")
    expect(schema.properties).toHaveProperty("waitForCompletion")
    
    // Variables should be an object with free-form properties
    expect(schema.properties.variables.type).toBe("object")
    expect(schema.properties.variables.additionalProperties).toBeDefined()
    
    // outputFormat should be an enum
    expect(schema.properties.outputFormat.enum).toContain("Markdown")
    expect(schema.properties.outputFormat.enum).toContain("RichText")
    expect(schema.properties.outputFormat.enum).toContain("PlainText")
    
    // aiActionId should be required
    expect(schema.required).toContain("aiActionId")
  })
  
  it("should have the correct schema for CREATE_AI_ACTION", () => {
    const tool = tools.CREATE_AI_ACTION
    
    expect(tool.name).toBe("create_ai_action")
    expect(tool.description).toContain("Create a new AI Action")
    
    const schema = tool.inputSchema
    expect(schema.properties).toHaveProperty("name")
    expect(schema.properties).toHaveProperty("description")
    expect(schema.properties).toHaveProperty("instruction")
    expect(schema.properties).toHaveProperty("configuration")
    
    // Instruction should have template and variables
    expect(schema.properties.instruction.properties).toHaveProperty("template")
    expect(schema.properties.instruction.properties).toHaveProperty("variables")
    
    // Configuration should have modelType and modelTemperature
    expect(schema.properties.configuration.properties).toHaveProperty("modelType")
    expect(schema.properties.configuration.properties).toHaveProperty("modelTemperature")
    
    // Required fields
    expect(schema.required).toContain("name")
    expect(schema.required).toContain("description")
    expect(schema.required).toContain("instruction")
    expect(schema.required).toContain("configuration")
  })
})
```

--------------------------------------------------------------------------------
/test/integration/bulk-action-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { expect, vi } from "vitest"
import { server } from "../msw-setup.js"

// Define mock values first - these can be before vi.mock
const TEST_ENTRY_ID = "test-entry-id"
const TEST_ASSET_ID = "test-asset-id"
const TEST_BULK_ACTION_ID = "test-bulk-action-id"

// Mock the client module without using variables defined later
vi.mock("../../src/config/client.ts", () => {
  return {
    getContentfulClient: vi.fn().mockResolvedValue({
      entry: {
        get: vi.fn().mockResolvedValue({
          sys: { id: "test-entry-id", version: 1 },
        }),
      },
      asset: {
        get: vi.fn().mockResolvedValue({
          sys: { id: "test-asset-id", version: 1 },
        }),
      },
      bulkAction: {
        publish: vi.fn().mockResolvedValue({
          sys: { id: "test-bulk-action-id", status: "created" },
        }),
        unpublish: vi.fn().mockResolvedValue({
          sys: { id: "test-bulk-action-id", status: "created" },
        }),
        validate: vi.fn().mockResolvedValue({
          sys: { id: "test-bulk-action-id", status: "created" },
        }),
        get: vi.fn().mockResolvedValue({
          sys: { id: "test-bulk-action-id", status: "succeeded" },
          succeeded: [
            { sys: { id: "test-entry-id", type: "Entry" } },
            { sys: { id: "test-asset-id", type: "Asset" } },
          ],
        }),
      },
    }),
  }
})

// Import handlers after mocking
import { bulkActionHandlers } from "../../src/handlers/bulk-action-handlers.ts"

describe("Bulk Action Handlers Integration Tests", () => {
  // Start MSW Server before tests
  beforeAll(() => server.listen())
  afterEach(() => server.resetHandlers())
  afterAll(() => server.close())

  const testSpaceId = "test-space-id"
  const testEnvironmentId = "master"

  describe("bulkPublish", () => {
    it("should publish multiple entries and assets", async () => {
      const result = await bulkActionHandlers.bulkPublish({
        spaceId: testSpaceId,
        environmentId: testEnvironmentId,
        entities: [
          { sys: { id: TEST_ENTRY_ID, type: "Entry" } },
          { sys: { id: TEST_ASSET_ID, type: "Asset" } },
        ],
      })

      expect(result).to.have.property("content").that.is.an("array")
      expect(result.content[0].text).to.include("Bulk publish completed")
      expect(result.content[0].text).to.include("Successfully processed")
    })
  })

  describe("bulkUnpublish", () => {
    it("should unpublish multiple entries and assets", async () => {
      const result = await bulkActionHandlers.bulkUnpublish({
        spaceId: testSpaceId,
        environmentId: testEnvironmentId,
        entities: [
          { sys: { id: TEST_ENTRY_ID, type: "Entry" } },
          { sys: { id: TEST_ASSET_ID, type: "Asset" } },
        ],
      })

      expect(result).to.have.property("content").that.is.an("array")
      expect(result.content[0].text).to.include("Bulk unpublish completed")
      expect(result.content[0].text).to.include("Successfully processed")
    })
  })

  describe("bulkValidate", () => {
    it("should validate multiple entries", async () => {
      const result = await bulkActionHandlers.bulkValidate({
        spaceId: testSpaceId,
        environmentId: testEnvironmentId,
        entryIds: [TEST_ENTRY_ID],
      })

      expect(result).to.have.property("content").that.is.an("array")
      expect(result.content[0].text).to.include("Bulk validation completed")
      expect(result.content[0].text).to.include("Successfully validated")
    })
  })
})

```

--------------------------------------------------------------------------------
/test/integration/client.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import { server } from "../msw-setup"

// Mock these modules at the top level
vi.mock("@contentful/node-apps-toolkit", () => ({
  getManagementToken: vi.fn(),
}))
vi.mock("contentful-management", () => ({
  createClient: vi.fn(),
}))

describe("getContentfulClient", () => {
  beforeEach(() => {
    server.listen()
  })

  afterEach(() => {
    server.resetHandlers()
    server.close()
    vi.resetModules()
    vi.clearAllMocks()
  })

  it("uses CONTENTFUL_MANAGEMENT_ACCESS_TOKEN if available", async () => {
    process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = "test-token"
    process.env.CONTENTFUL_HOST = "api.contentful.com"

    const mockCreateClient = vi.fn()
    const { createClient } = await import("contentful-management")
    vi.mocked(createClient).mockImplementation(mockCreateClient)

    const { getContentfulClient } = await import("../../src/config/client")
    await getContentfulClient()

    expect(mockCreateClient).toHaveBeenCalledWith(
      {
        accessToken: "test-token",
        host: "api.contentful.com",
        headers: {
          "X-Contentful-user-agent": "contentful-community-mcp/1.0.0",
        },
      },
      { type: "plain" },
    )
  })

  it("gets a token using PRIVATE_KEY and APP_ID if MANAGEMENT_ACCESS_TOKEN not available", async () => {
    delete process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN
    process.env.PRIVATE_KEY = "test-private-key"
    process.env.APP_ID = "test-app-id"
    process.env.SPACE_ID = "test-space-id"
    process.env.ENVIRONMENT_ID = "test-environment-id"
    process.env.CONTENTFUL_HOST = "api.contentful.com"

    const { getManagementToken } = await import("@contentful/node-apps-toolkit")
    const { createClient } = await import("contentful-management")

    vi.mocked(getManagementToken).mockResolvedValue("generated-token")
    const mockCreateClient = vi.fn()
    vi.mocked(createClient).mockImplementation(mockCreateClient)

    const { getContentfulClient } = await import("../../src/config/client")
    await getContentfulClient()

    expect(getManagementToken).toHaveBeenCalledWith(
      "-----BEGIN RSA PRIVATE KEY-----\ntest-private-key\n-----END RSA PRIVATE KEY-----",
      {
        appInstallationId: "test-app-id",
        spaceId: "test-space-id",
        environmentId: "test-environment-id",
        host: "https://api.contentful.com",
      },
    )

    expect(mockCreateClient).toHaveBeenCalledWith(
      {
        accessToken: "generated-token",
        host: "api.contentful.com",
        headers: {
          "X-Contentful-user-agent": "contentful-community-mcp/1.0.0",
        },
      },
      { type: "plain" },
    )
  })

  it("includes MCP identification header in all client configurations", async () => {
    process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = "test-token"
    process.env.CONTENTFUL_HOST = "api.contentful.com"

    const mockCreateClient = vi.fn()
    const { createClient } = await import("contentful-management")
    vi.mocked(createClient).mockImplementation(mockCreateClient)

    const { getContentfulClient } = await import("../../src/config/client")
    await getContentfulClient()

    const callArgs = mockCreateClient.mock.calls[0]
    const config = callArgs[0]

    expect(config.headers).toBeDefined()
    expect(config.headers["X-Contentful-user-agent"]).toBe("contentful-community-mcp/1.0.0")
  })

  it("throws if neither CONTENTFUL_MANAGEMENT_ACCESS_TOKEN nor PRIVATE_KEY is available", async () => {
    delete process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN
    delete process.env.PRIVATE_KEY

    vi.resetModules()

    const { getContentfulClient } = await import("../../src/config/client")

    await expect(() => getContentfulClient()).rejects.toThrow(
      "No Contentful management token or private key found...",
    )
  })
})

```

--------------------------------------------------------------------------------
/src/prompts/ai-actions-overview.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Handler for providing a comprehensive overview of AI Actions in Contentful
 */
export async function handleAiActionsOverview() {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful AI Actions specialist. I can explain how AI Actions integrate AI capabilities directly into your content management workflows.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `Please explain what AI Actions are in Contentful, how they work, and how they relate to content entries and fields.`,
        },
      },
      {
        role: "assistant",
        content: {
          type: "text",
          text: `# AI Actions in Contentful

AI Actions are predefined AI operations that can be integrated directly into your Contentful content workflows. They allow content creators to leverage AI capabilities without needing technical expertise in prompt engineering or AI integration.

## Key Concepts

1. **Templates with Variables**: Each AI Action contains a prompt template with placeholders for variables that users can provide when invoking the action.

2. **Content Integration**: AI Actions operate on content entries and fields in your Contentful space. They can read content from specific fields and generate new content based on that input.

3. **Asynchronous Processing**: When invoked, AI Actions run asynchronously in the background, allowing content editors to continue working while waiting for results.

4. **Flexible Variable Types**: Support for various input types:
   - Simple text inputs
   - References to other entries
   - Media references
   - Locale selection
   - Predefined option lists

5. **Field-Level Operations**: AI Actions can be applied to specific fields within entries using the entity path parameter.

## How AI Actions Work

1. **Creation**: Developers or content managers define AI Actions with:
   - A name and description
   - A prompt template
   - Variable definitions
   - AI model configuration (model type, temperature)

2. **Publication**: Actions are published to make them available to content editors.

3. **Invocation**: Content editors can:
   - Select an AI Action from the UI
   - Fill in required variables
   - Apply it to specific content
   - Receive AI-generated content they can review and incorporate

4. **Results**: The AI-generated content is presented to the editor who can then:
   - Accept it as is
   - Edit it further
   - Reject it and try again with different parameters

## Practical Applications

- Generating SEO metadata from existing content
- Creating alt text for images
- Translating content between languages
- Summarizing long-form content
- Enhancing product descriptions
- Creating variations of existing content
- Improving grammar and readability

## Using AI Actions via MCP

When using this MCP integration, you can:
1. Create and manage AI Actions using the management tools
2. Invoke existing AI Actions on specific content
3. Process the results for further use

Each published AI Action becomes available as a dynamic tool with its own parameters based on its variable definitions.

## Working with Complex Variables

One of the most important aspects to understand is how to work with References and MediaReferences:

- They require both an ID parameter (which entry/asset to use)
- And a path parameter (which field within that entry/asset to access)

This two-part approach gives you precise control over what content is processed by the AI Action.

## Understanding the Output

AI Actions return results for review, but they don't automatically update fields in your entries. This gives editors control over what content actually gets published.

Would you like me to explain any specific aspect of AI Actions in more detail?`,
        },
      },
    ],
  };
}
```

--------------------------------------------------------------------------------
/test/integration/asset-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { expect } from "vitest";
import { assetHandlers } from "../../src/handlers/asset-handlers.js";
import { server } from "../msw-setup.js";

describe("Asset Handlers Integration Tests", () => {
  // Start MSW Server before tests
  beforeAll(() => server.listen());
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  const testSpaceId = "test-space-id";
  const testAssetId = "test-asset-id";

  describe("uploadAsset", () => {
    it("should upload and process a new asset", async () => {
      const uploadData = {
        spaceId: testSpaceId,
        title: "Test Asset",
        description: "Test Description",
        file: {
          fileName: "test.jpg",
          contentType: "image/jpeg",
          upload: "https://example.com/test.jpg",
        },
      };

      const result = await assetHandlers.uploadAsset(uploadData);

      // Verify the response structure
      expect(result).to.have.property("content").that.is.an("array");
      expect(result.content).to.have.lengthOf(1);
      expect(result.content[0]).to.have.property("type", "text");

      // Parse and verify the asset data
      const asset = JSON.parse(result.content[0].text);
      expect(asset).to.have.nested.property("sys.id", "test-asset-id");
      expect(asset).to.have.nested.property("sys.version").that.is.a("number");
      expect(asset).to.have.nested.property("fields.title.en-US", "Test Asset");
      expect(asset).to.have.nested.property(
        "fields.description.en-US",
        "Test Description",
      );
      expect(asset).to.have.nested.property("fields.file.en-US").that.includes({
        fileName: "test.jpg",
        contentType: "image/jpeg",
      });
    });
  });
  describe("getAsset", () => {
    it("should get details of a specific asset", async () => {
      const result = await assetHandlers.getAsset({
        spaceId: testSpaceId,
        assetId: testAssetId,
      });

      expect(result).to.have.property("content");
      const asset = JSON.parse(result.content[0].text);
      expect(asset.sys.id).to.equal(testAssetId);
    });

    it("should throw error for invalid asset ID", async () => {
      try {
        await assetHandlers.getAsset({
          spaceId: testSpaceId,
          assetId: "invalid-asset-id",
        });
        expect.fail("Should have thrown an error");
      } catch (error) {
        expect(error).to.exist;
      }
    });
  });

  describe("updateAsset", () => {
    it("should update an existing asset", async () => {
      const result = await assetHandlers.updateAsset({
        spaceId: testSpaceId,
        assetId: testAssetId,
        title: "Updated Asset",
        description: "Updated Description",
      });

      expect(result).to.have.property("content");
      const asset = JSON.parse(result.content[0].text);
      expect(asset.fields.title["en-US"]).to.equal("Updated Asset");
      expect(asset.fields.description["en-US"]).to.equal("Updated Description");
    });
  });

  describe("deleteAsset", () => {
    it("should delete an asset", async () => {
      const result = await assetHandlers.deleteAsset({
        spaceId: testSpaceId,
        assetId: testAssetId,
      });

      expect(result).to.have.property("content");
      expect(result.content[0].text).to.include("deleted successfully");
    });
  });

  describe("publishAsset", () => {
    it("should publish an asset", async () => {
      const result = await assetHandlers.publishAsset({
        spaceId: testSpaceId,
        assetId: testAssetId,
      });

      expect(result).to.have.property("content");
      const asset = JSON.parse(result.content[0].text);
      expect(asset.sys.publishedVersion).to.exist;
    });
  });

  describe("unpublishAsset", () => {
    it("should unpublish an asset", async () => {
      const result = await assetHandlers.unpublishAsset({
        spaceId: testSpaceId,
        assetId: testAssetId,
      });

      expect(result).to.have.property("content");
      const asset = JSON.parse(result.content[0].text);
      expect(asset.sys.publishedVersion).to.not.exist;
    });
  });
});

```

--------------------------------------------------------------------------------
/test/unit/ai-actions.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest"
import type {
  AiActionEntity,
  Variable,
  Instruction,
  Configuration,
  VariableValue,
  AiActionInvocation
} from "../../src/types/ai-actions"

describe("AI Action Types", () => {
  it("should validate Variable type structure", () => {
    // Test creating variables of different types
    const textVariable: Variable = {
      id: "text-var",
      type: "Text",
      name: "Text Variable",
      description: "A text variable"
    }

    const optionsVariable: Variable = {
      id: "options-var",
      type: "StringOptionsList",
      name: "Options Variable",
      configuration: {
        values: ["option1", "option2", "option3"],
        allowFreeFormInput: false
      }
    }

    const referenceVariable: Variable = {
      id: "ref-var",
      type: "Reference",
      name: "Reference Variable",
      configuration: {
        allowedEntities: ["Entry"]
      } as any
    }

    expect(textVariable.id).toBe("text-var")
    expect(optionsVariable.type).toBe("StringOptionsList")
    expect((referenceVariable.configuration as any)?.allowedEntities).toContain("Entry")
  })

  it("should validate Instruction type structure", () => {
    const instruction: Instruction = {
      template: "This is a template with {{var1}} and {{var2}}",
      variables: [
        { id: "var1", type: "Text" },
        { id: "var2", type: "StandardInput" }
      ],
      conditions: [
        { id: "cond1", variable: "var1", operator: "eq", value: "some value" }
      ]
    }

    expect(instruction.template).toContain("{{var1}}")
    expect(instruction.variables).toHaveLength(2)
    expect(instruction.conditions?.[0].operator).toBe("eq")
  })

  it("should validate Configuration type structure", () => {
    const config: Configuration = {
      modelType: "gpt-4",
      modelTemperature: 0.7
    }

    expect(config.modelType).toBe("gpt-4")
    expect(config.modelTemperature).toBe(0.7)
  })

  it("should validate variable value structure", () => {
    const textValue: VariableValue = {
      id: "text-var",
      value: "some text value"
    }

    const refValue: VariableValue = {
      id: "ref-var",
      value: {
        entityType: "Entry",
        entityId: "entry123",
        entityPath: "fields.title"
      }
    }

    expect(textValue.value).toBe("some text value")
    expect(refValue.value.entityType).toBe("Entry")
  })

  it("should validate AiActionEntity structure", () => {
    const entity: AiActionEntity = {
      sys: {
        id: "action1",
        type: "AiAction",
        createdAt: "2023-01-01T00:00:00Z",
        updatedAt: "2023-01-02T00:00:00Z",
        version: 1,
        space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
        createdBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
        updatedBy: { sys: { id: "user1", linkType: "User", type: "Link" } }
      },
      name: "Test Action",
      description: "A test action",
      instruction: {
        template: "Template with {{var}}",
        variables: [{ id: "var", type: "Text" }]
      },
      configuration: {
        modelType: "gpt-4",
        modelTemperature: 0.5
      }
    }

    expect(entity.sys.id).toBe("action1")
    expect(entity.name).toBe("Test Action")
    expect(entity.instruction.variables).toHaveLength(1)
  })

  it("should validate AiActionInvocation structure", () => {
    const invocation: AiActionInvocation = {
      sys: {
        id: "inv1",
        type: "AiActionInvocation",
        space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
        environment: { sys: { id: "master", linkType: "Environment", type: "Link" } },
        aiAction: { sys: { id: "action1", linkType: "AiAction", type: "Link" } },
        status: "COMPLETED"
      },
      result: {
        type: "text",
        content: "Generated content",
        metadata: {
          invocationResult: {
            aiAction: {
              sys: {
                id: "action1",
                linkType: "AiAction",
                type: "Link",
                version: 1
              }
            },
            outputFormat: "PlainText",
            promptTokens: 50,
            completionTokens: 100,
            modelId: "gpt-4",
            modelProvider: "OpenAI"
          }
        }
      }
    }

    expect(invocation.sys.status).toBe("COMPLETED")
    expect(invocation.result?.content).toBe("Generated content")
    expect(invocation.result?.metadata.invocationResult.promptTokens).toBe(50)
  })
})
```

--------------------------------------------------------------------------------
/test/unit/entry-handler-merge.test.ts:
--------------------------------------------------------------------------------

```typescript
import { expect, vi, describe, it, beforeEach } from "vitest"
import { entryHandlers } from "../../src/handlers/entry-handlers.js"

// Define test constants 
const TEST_ENTRY_ID = "test-entry-id"
const TEST_SPACE_ID = "test-space-id"
const TEST_ENV_ID = "master"

// Move vi.mock call to top level - this gets hoisted automatically
vi.mock("../../src/config/client.js", () => {
  return {
    getContentfulClient: vi.fn().mockImplementation(() => {
      // Create mock entry when the function is called
      const mockEntry = {
        sys: { id: TEST_ENTRY_ID, version: 1 },
        fields: {
          title: { "en-US": "Original Title" },
          description: { "en-US": "Original Description" },
          tags: { "en-US": ["tag1", "tag2"] }
        }
      }

      return {
        entry: {
          get: vi.fn().mockResolvedValue(mockEntry),
          update: vi.fn().mockImplementation((params, entryProps) => {
            // Return a merged entry that simulates the updated fields
            return Promise.resolve({
              sys: { id: params.entryId, version: 2 },
              fields: entryProps.fields
            })
          })
        }
      }
    })
  }
})

describe("Entry Handler Merge Logic", () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it("should merge existing fields with update fields when only partial update is provided", async () => {
    // Setup - just update the title but not other fields
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      entryId: TEST_ENTRY_ID,
      fields: {
        title: { "en-US": "Updated Title" }
      }
    }

    // Execute
    const result = await entryHandlers.updateEntry(updateData)
    
    // Parse the result
    const updatedEntry = JSON.parse(result.content[0].text)
    
    // Assert - should have updated title but kept original description and tags
    expect(updatedEntry.fields.title["en-US"]).toEqual("Updated Title")
    expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description")
    expect(updatedEntry.fields.tags["en-US"]).toEqual(["tag1", "tag2"])
  })

  it("should handle updates to nested locale fields", async () => {
    // Setup - update a specific locale but not others
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      entryId: TEST_ENTRY_ID,
      fields: {
        title: { 
          "de-DE": "Deutscher Titel" // Add a new locale
        }
      }
    }

    // Execute
    const result = await entryHandlers.updateEntry(updateData)
    
    // Parse the result
    const updatedEntry = JSON.parse(result.content[0].text)
    
    // Assert - should merge the locales in the title field
    expect(updatedEntry.fields.title["en-US"]).toEqual("Original Title") // Kept original locale
    expect(updatedEntry.fields.title["de-DE"]).toEqual("Deutscher Titel") // Added new locale
    expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description") // Kept other fields
  })

  it("should handle adding a new field", async () => {
    // Setup - add a completely new field
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      entryId: TEST_ENTRY_ID,
      fields: {
        newField: { "en-US": "New Field Value" }
      }
    }

    // Execute
    const result = await entryHandlers.updateEntry(updateData)
    
    // Parse the result
    const updatedEntry = JSON.parse(result.content[0].text)
    
    // Assert - should have original fields plus the new field
    expect(updatedEntry.fields.title["en-US"]).toEqual("Original Title")
    expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description")
    expect(updatedEntry.fields.newField["en-US"]).toEqual("New Field Value")
  })

  it("should handle updating an array field", async () => {
    // Setup - update the tags array
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      entryId: TEST_ENTRY_ID,
      fields: {
        tags: { "en-US": ["tag1", "tag2", "tag3"] } // Add tag3
      }
    }

    // Execute
    const result = await entryHandlers.updateEntry(updateData)
    
    // Parse the result
    const updatedEntry = JSON.parse(result.content[0].text)
    
    // Assert - should have updated tags but kept other fields
    expect(updatedEntry.fields.title["en-US"]).toEqual("Original Title")
    expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description")
    expect(updatedEntry.fields.tags["en-US"]).toEqual(["tag1", "tag2", "tag3"])
  })
})
```

--------------------------------------------------------------------------------
/src/types/ai-actions.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Type definitions for Contentful AI Actions
 */

// Variable types
export type VariableType =
  | "ResourceLink"
  | "Text"
  | "StringOptionsList"
  | "FreeFormInput"
  | "StandardInput"
  | "Locale"
  | "MediaReference"
  | "Reference"
  | "SmartContext";

// Entity type for reference variables
export type EntityType = "Entry" | "Asset" | "ResourceLink";

// Variable configuration types
export type StringOptionsListConfiguration = {
  allowFreeFormInput?: boolean;
  values: string[];
};

export type TextConfiguration = {
  strict: boolean;
  in: string[];
};

export type ReferenceConfiguration = {
  allowedEntities: EntityType[];
};

export type VariableConfiguration =
  | StringOptionsListConfiguration
  | TextConfiguration
  | ReferenceConfiguration;

// Variable definition
export interface Variable {
  id: string;
  type: VariableType;
  name?: string;
  description?: string;
  configuration?: VariableConfiguration;
}

// Condition for conditional template sections
export type ConditionOperator = "eq" | "neq" | "in" | "nin";

export interface StringCondition {
  id: string;
  variable: string;
  operator: "eq" | "neq";
  value: string;
}

export interface ArrayCondition {
  id: string;
  variable: string;
  operator: "in" | "nin";
  value: string[];
}

export type Condition = StringCondition | ArrayCondition;

// Instruction that contains the template and variables
export interface Instruction {
  template: string;
  variables: Variable[];
  conditions?: Condition[];
}

// Model configuration
export interface Configuration {
  modelType: string;
  modelTemperature: number;
}

// Input variable value types
export interface TextVariableValue {
  id: string;
  value: string;
}

export interface ReferenceVariableValue {
  id: string;
  value: {
    entityType: EntityType;
    entityId: string;
    entityPath?: string;
    entityPaths?: string[];
  };
}

export type VariableValue = TextVariableValue | ReferenceVariableValue;

// Output format for AI Action results
export type OutputFormat = "RichText" | "Markdown" | "PlainText";

// Invocation request
export interface AiActionInvocationType {
  outputFormat?: OutputFormat;
  variables?: VariableValue[];
}

// AI Action test case
export interface TextTestCase {
  type: "Text";
  value: string;
}

export interface ReferenceTestCase {
  type: "Reference";
  value: {
    entityType: EntityType;
    entityId: string;
    entityPath?: string;
    entityPaths?: string[];
  };
}

export type AiActionTestCase = TextTestCase | ReferenceTestCase;

// Status for invocations and filtering
export type InvocationStatus = "SCHEDULED" | "IN_PROGRESS" | "FAILED" | "COMPLETED" | "CANCELLED";
export type StatusFilter = "all" | "published";

// System links
export interface SysLink {
  sys: {
    id: string;
    linkType: string;
    type: "Link";
  };
}

export interface VersionedLink extends SysLink {
  sys: {
    id: string;
    linkType: string;
    type: "Link";
    version: number;
  };
}

// AI Action entity
export interface AiActionEntity {
  sys: {
    id: string;
    type: "AiAction";
    createdAt: string;
    updatedAt: string;
    version: number;
    space: SysLink;
    createdBy: SysLink;
    updatedBy: SysLink;
    publishedAt?: string;
    publishedVersion?: number;
    publishedBy?: SysLink;
  };
  name: string;
  description: string;
  instruction: Instruction;
  configuration: Configuration;
  testCases?: AiActionTestCase[];
}

export interface AiActionEntityCollection {
  sys: {
    type: "Array";
  };
  items: AiActionEntity[];
  skip?: number;
  limit?: number;
  total?: number;
}

// AI Action creation/update schema
export interface AiActionSchemaParsed {
  name: string;
  description: string;
  instruction: Instruction;
  configuration: Configuration;
  testCases?: AiActionTestCase[];
}

// Rich text components
export interface Mark {
  type: string;
}

export interface Text {
  nodeType: "text";
  value: string;
  marks: Mark[];
  data: Record<string, unknown>;
}

export interface Node {
  nodeType: string;
  data: Record<string, unknown>;
  content: (Node | Text)[];
}

export interface RichTextDocument {
  nodeType: "document";
  data: Record<string, unknown>;
  content: Node[];
}

// Result types
export interface AiActionInvocationMetadata {
  invocationResult: {
    aiAction: VersionedLink;
    outputFormat: OutputFormat;
    promptTokens: number;
    completionTokens: number;
    modelId: string;
    modelProvider: string;
    outputMetadata?: {
      customNodesMap: Record<string, Node>;
    };
  };
  statusChangedDates?: Array<{
    date: string;
    status: InvocationStatus;
  }>;
}

export interface FlowResult {
  type: "text";
  content: string | RichTextDocument;
  metadata: AiActionInvocationMetadata;
}

export interface AiActionInvocation {
  sys: {
    id: string;
    type: "AiActionInvocation";
    space: SysLink;
    environment: SysLink;
    aiAction: SysLink;
    status: InvocationStatus;
    errorCode?: string;
  };
  result?: FlowResult;
}
```

--------------------------------------------------------------------------------
/test/integration/content-type-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { expect } from "vitest"
import { contentTypeHandlers } from "../../src/handlers/content-type-handlers.js"
import { server } from "../msw-setup.js"
import { toCamelCase } from "../../src/utils/to-camel-case.js"

describe("Content Type Handlers Integration Tests", () => {
  // Start MSW Server before tests
  beforeAll(() => server.listen())
  afterEach(() => server.resetHandlers())
  afterAll(() => server.close())

  const testSpaceId = "test-space-id"
  const testContentTypeId = "test-content-type-id"

  describe("listContentTypes", () => {
    it("should list all content types", async () => {
      const result = await contentTypeHandlers.listContentTypes({
        spaceId: testSpaceId,
      })

      expect(result).to.have.property("content").that.is.an("array")
      expect(result.content).to.have.lengthOf(1)

      const contentTypes = JSON.parse(result.content[0].text)
      expect(contentTypes.items).to.be.an("array")
      expect(contentTypes.items[0]).to.have.nested.property("sys.id", "test-content-type-id")
      expect(contentTypes.items[0]).to.have.property("name", "Test Content Type")
    })
  })

  describe("getContentType", () => {
    it("should get details of a specific content type", async () => {
      const result = await contentTypeHandlers.getContentType({
        spaceId: testSpaceId,
        contentTypeId: testContentTypeId,
      })

      expect(result).to.have.property("content")
      const contentType = JSON.parse(result.content[0].text)
      expect(contentType).to.have.nested.property("sys.id", toCamelCase(testContentTypeId))
      expect(contentType).to.have.property("name", "Test Content Type")
      expect(contentType.fields).to.be.an("array")
    })

    it("should throw error for invalid content type ID", async () => {
      try {
        await contentTypeHandlers.getContentType({
          spaceId: testSpaceId,
          contentTypeId: "invalid-id",
        })
        expect.fail("Should have thrown an error")
      } catch (error) {
        expect(error).to.exist
      }
    })
  })

  describe("createContentType", () => {
    it("should create a new content type", async () => {
      const contentTypeData = {
        spaceId: testSpaceId,
        name: "New Content Type",
        fields: [
          {
            id: "title",
            name: "Title",
            type: "Text",
            required: true,
          },
        ],
      }

      const result = await contentTypeHandlers.createContentType(contentTypeData)

      expect(result).to.have.property("content")
      const contentType = JSON.parse(result.content[0].text)
      expect(contentType).to.have.nested.property("sys.id", "newContentType")
      expect(contentType).to.have.property("name", "New Content Type")
      expect(contentType.fields).to.be.an("array")
    })
  })

  describe("updateContentType", () => {
    it("should update an existing content type", async () => {
      const updateData = {
        spaceId: testSpaceId,
        contentTypeId: testContentTypeId,
        name: "Updated Content Type",
        fields: [
          {
            id: "title",
            name: "Updated Title",
            type: "Text",
            required: true,
          },
        ],
      }

      const result = await contentTypeHandlers.updateContentType(updateData)

      expect(result).to.have.property("content")
      const contentType = JSON.parse(result.content[0].text)
      expect(contentType).to.have.nested.property("sys.id", testContentTypeId)
      expect(contentType).to.have.property("name", "Updated Content Type")
    })
  })

  describe("deleteContentType", () => {
    it("should delete a content type", async () => {
      const result = await contentTypeHandlers.deleteContentType({
        spaceId: testSpaceId,
        contentTypeId: testContentTypeId,
      })

      expect(result).to.have.property("content")
      expect(result.content[0].text).to.include("deleted successfully")
    })

    it("should throw error when deleting non-existent content type", async () => {
      try {
        await contentTypeHandlers.deleteContentType({
          spaceId: testSpaceId,
          contentTypeId: "non-existent-id",
        })
        expect.fail("Should have thrown an error")
      } catch (error) {
        expect(error).to.exist
      }
    })
  })

  describe("publishContentType", () => {
    it("should publish a content type", async () => {
      const result = await contentTypeHandlers.publishContentType({
        spaceId: testSpaceId,
        contentTypeId: testContentTypeId,
      })

      expect(result).to.have.property("content")
      expect(result.content[0].text).to.include("published successfully")
    })

    it("should throw error when publishing non-existent content type", async () => {
      try {
        await contentTypeHandlers.publishContentType({
          spaceId: testSpaceId,
          contentTypeId: "non-existent-id",
        })
        expect.fail("Should have thrown an error")
      } catch (error) {
        expect(error).to.exist
      }
    })
  })
})

```

--------------------------------------------------------------------------------
/test/integration/space-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { expect } from "vitest";
import { spaceHandlers } from "../../src/handlers/space-handlers.js";
import { server } from "../msw-setup.js";

describe("Space Handlers Integration Tests", () => {
  // Store spaceId for use in other tests
  let testSpaceId: string;

  // Start MSW Server before tests
  beforeAll(() => server.listen());
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  describe("listSpaces", () => {
    it("should list all available spaces", async () => {
      const result = await spaceHandlers.listSpaces();
      expect(result).to.have.property("content");
      expect(result.content[0]).to.have.property("type", "text");

      const spaces = JSON.parse(result.content[0].text);
      expect(spaces).to.have.property("items");
      expect(Array.isArray(spaces.items)).to.be.true;

      // Store the first space ID for subsequent tests
      if (spaces.items.length > 0) {
        testSpaceId = spaces.items[0].sys.id;
      }
    });
  });

  describe("getSpace", () => {
    it("should get details of a specific space", async () => {
      // Skip if no test space is available
      if (!testSpaceId) {
        return;
      }

      const result = await spaceHandlers.getSpace({ spaceId: testSpaceId });
      expect(result).to.have.property("content");
      expect(result.content[0]).to.have.property("type", "text");

      const spaceDetails = JSON.parse(result.content[0].text);
      expect(spaceDetails).to.have.property("sys");
      expect(spaceDetails.sys).to.have.property("id", testSpaceId);
    });

    it("should throw error for invalid space ID", async () => {
      try {
        await spaceHandlers.getSpace({ spaceId: "invalid-space-id" });
        expect.fail("Should have thrown an error");
      } catch (error) {
        expect(error).to.exist;
      }
    });
  });

  describe("listEnvironments", () => {
    it("should list environments for a space", async () => {
      // Skip if no test space is available
      if (!testSpaceId) {
        return;
      }

      const result = await spaceHandlers.listEnvironments({
        spaceId: testSpaceId,
      });
      expect(result).to.have.property("content");
      expect(result.content[0]).to.have.property("type", "text");

      const environments = JSON.parse(result.content[0].text);
      expect(environments).to.have.property("items");
      expect(Array.isArray(environments.items)).to.be.true;

      // Verify master environment exists
      const masterEnv = environments.items.find(
        (env: any) => env.sys.id === "master",
      );
      expect(masterEnv).to.exist;
    });

    it("should throw error for invalid space ID", async () => {
      try {
        await spaceHandlers.listEnvironments({ spaceId: "invalid-space-id" });
        expect.fail("Should have thrown an error");
      } catch (error) {
        expect(error).to.exist;
      }
    });
  });

  describe("createEnvironment", () => {
    it("should create a new environment", async () => {
      // Skip if no test space is available
      if (!testSpaceId) {
        return;
      }

      const envName = `test-env-${Date.now()}`;
      const result = await spaceHandlers.createEnvironment({
        spaceId: testSpaceId,
        environmentId: envName,
        name: envName,
      });

      expect(result).to.have.property("content");
      expect(result.content[0]).to.have.property("type", "text");

      const environment = JSON.parse(result.content[0].text);
      expect(environment).to.have.property("sys");
      expect(environment.sys).to.have.property("id", envName);
      expect(environment).to.have.property("name", envName);

      // Store environment ID for deletion test
      return envName;
    });

    it("should throw error for invalid space ID", async () => {
      try {
        await spaceHandlers.createEnvironment({
          spaceId: "invalid-space-id",
          environmentId: "test-env",
          name: "Test Environment",
        });
        expect.fail("Should have thrown an error");
      } catch (error) {
        expect(error).to.exist;
      }
    });
  });

  describe("deleteEnvironment", () => {
    it("should delete an environment", async () => {
      // Skip if no test space is available
      if (!testSpaceId) {
        return;
      }

      // Create a temporary environment
      const envName = `temp-env-${Date.now()}`;
      await spaceHandlers.createEnvironment({
        spaceId: testSpaceId,
        environmentId: envName,
        name: envName,
      });

      // Delete the environment
      const result = await spaceHandlers.deleteEnvironment({
        spaceId: testSpaceId,
        environmentId: envName,
      });

      expect(result).to.have.property("content");
      expect(result.content[0]).to.have.property("type", "text");
      expect(result.content[0].text).to.include("deleted successfully");
    });

    it("should throw error for invalid environment ID", async () => {
      if (!testSpaceId) {
        return;
      }

      try {
        await spaceHandlers.deleteEnvironment({
          spaceId: testSpaceId,
          environmentId: "non-existent-env",
        });
        expect.fail("Should have thrown an error");
      } catch (error) {
        expect(error).to.exist;
      }
    });
  });
});

```

--------------------------------------------------------------------------------
/src/prompts/ai-actions-invoke.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Handler for invoking AI Actions in Contentful
 */
export async function handleAiActionsInvoke(actionId?: string, details?: string) {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful AI Actions execution expert. I can help you invoke AI Actions, provide the right parameters, and understand the results. I know how to work with both simple and complex variable types, including references and entity paths.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need help with invoking an AI Action ${actionId ? `with ID ${actionId}` : "in my Contentful space"}. ${details || "Please guide me through providing variables correctly, handling references, and interpreting the results."}`
        },
      },
      {
        role: "assistant",
        content: {
          type: "text",
          text: `# Invoking AI Actions in Contentful${actionId ? `: ${actionId}` : ""}

I'll guide you through the process of effectively invoking AI Actions through the MCP interface.

## Finding Available AI Actions

Before invoking an AI Action, you need to know which ones are available in your space. You can discover them using:

\`\`\`javascript
list_ai_actions({
  spaceId: "your-space-id",
  environmentId: "master", // typically "master" or your environment name
  status: "published" // only show published (available) actions
});
\`\`\`

This will return a list of AI Actions with their IDs, names, descriptions, and other metadata.

## Understanding An AI Action's Requirements

Once you've identified the AI Action you want to use${actionId ? ` (in this case, ${actionId})` : ""}, you can get its details:

\`\`\`javascript
get_ai_action({
  spaceId: "your-space-id",
  environmentId: "master",
  aiActionId: ${actionId ? `"${actionId}"` : `"the-action-id"`}
});
\`\`\`

This will show you the full definition, including all required variables and their types.

## Dynamic AI Action Tools

In the MCP implementation, each published AI Action becomes available as a dynamic tool with the prefix \`ai_action_\` followed by the AI Action ID. For example, an AI Action with ID "3woPNtzC81CEsBEvgQo96J" would be accessible as:

\`\`\`javascript
ai_action_3woPNtzC81CEsBEvgQo96J({
  // parameters based on the AI Action's variables
});
\`\`\`

## Preparing Parameters

AI Actions require specific parameters based on their variable definitions:

### Basic Variable Types

For simple variable types (Text, FreeFormInput, StringOptionsList, Locale), provide the values directly:

\`\`\`javascript
ai_action_example({
  tone: "Professional", // StringOptionsList
  target_audience: "Enterprise customers", // Text
  locale: "en-US" // Locale
});
\`\`\`

### Reference Variables

For Reference variables (linking to other entries), you need to provide:

1. The entry ID
2. The field path to access

\`\`\`javascript
ai_action_example({
  product_entry: "6tFnSQdgHuWYOk8eICA0w", // Entry ID
  product_entry_path: "fields.description.en-US" // Field path
});
\`\`\`

### Media Reference Variables

Similarly, for MediaReference variables (images, videos, etc.):

\`\`\`javascript
ai_action_example({
  product_image: "7tGnRQegIvWZPj9eICA1q", // Asset ID
  product_image_path: "fields.file.en-US" // Field path
});
\`\`\`

### Standard Input

For the main content (StandardInput):

\`\`\`javascript
ai_action_example({
  input_text: "Your content to process..."
});
\`\`\`

## Complete Example

Here's a complete example of invoking an AI Action:

\`\`\`javascript
ai_action_content_enhancer({
  // Basic parameters
  input_text: "Original content to enhance...",
  tone: "Professional",
  
  // Reference to another entry
  brand_guidelines: "1aBcDeFgHiJkLmNoPqR",
  brand_guidelines_path: "fields.guidelines.en-US",
  
  // Additional settings
  outputFormat: "Markdown", // Output format (Markdown, RichText, or PlainText)
  waitForCompletion: true // Wait for processing to complete
});
\`\`\`

## Output Formats

You can specify how you want the output formatted using the \`outputFormat\` parameter:

- **Markdown**: Clean, formatted text with markdown syntax (default)
- **RichText**: Contentful's structured rich text format
- **PlainText**: Simple text without formatting

## Asynchronous Processing

AI Actions process asynchronously by default. You can control this behavior with:

- **waitForCompletion**: Set to \`true\` to wait for the operation to complete (default)
- If set to \`false\`, you'll receive an invocation ID that you can use to check status later

## Getting Results Later

If you opted not to wait for completion, you can check the status later:

\`\`\`javascript
get_ai_action_invocation({
  spaceId: "your-space-id",
  environmentId: "master",
  aiActionId: "the-action-id",
  invocationId: "the-invocation-id" // Received from the initial invoke call
});
\`\`\`

## Important Notes

1. **Results are not automatically applied**: AI Action results are returned for you to review but aren't automatically applied to entries.

2. **Field paths are crucial**: When working with References and MediaReferences, always provide the correct field path.

3. **Check for required variables**: All required variables must be provided, or the invocation will fail.

4. **Response times vary**: Complex AI Actions may take longer to process.

Does this help with your AI Action invocation? Would you like more specific guidance on any of these aspects?`,
        },
      },
    ],
  };
}
```

--------------------------------------------------------------------------------
/src/prompts/contentful-prompts.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Prompt definitions for the Contentful MCP server
 * These prompts help guide users through common operations and concepts
 */
export const CONTENTFUL_PROMPTS = {
  "explain-api-concepts": {
    name: "explain-api-concepts",
    description: "Explain Contentful API concepts and relationships",
    arguments: [
      {
        name: "concept",
        description: "Contentful concept (Space/Environment/ContentType/Entry/Asset)",
        required: true
      }
    ]
  },
  "space-identification": {
    name: "space-identification",
    description: "Guide for identifying the correct Contentful space for operations",
    arguments: [
      {
        name: "operation",
        description: "Operation you want to perform",
        required: true
      }
    ]
  },
  "content-modeling-guide": {
    name: "content-modeling-guide",
    description: "Guide through content modeling decisions and best practices",
    arguments: [
      {
        name: "useCase",
        description: "Description of the content modeling scenario",
        required: true
      }
    ]
  },
  "api-operation-help": {
    name: "api-operation-help",
    description: "Get detailed help for specific Contentful API operations",
    arguments: [
      {
        name: "operation",
        description: "API operation (CRUD, publish, archive, etc)",
        required: true
      },
      {
        name: "resourceType",
        description: "Type of resource (Entry/Asset/ContentType)",
        required: true
      }
    ]
  },
  "entry-management": {
    name: "entry-management",
    description: "Help with CRUD operations and publishing workflows for content entries",
    arguments: [
      {
        name: "task",
        description: "Specific task (create/read/update/delete/publish/unpublish/bulk)",
        required: false
      },
      {
        name: "details",
        description: "Additional context or requirements",
        required: false
      }
    ]
  },
  "asset-management": {
    name: "asset-management",
    description: "Guidance on managing digital assets like images, videos, and documents",
    arguments: [
      {
        name: "task",
        description: "Specific task (upload/process/update/delete/publish)",
        required: false
      },
      {
        name: "details",
        description: "Additional context about asset types or requirements",
        required: false
      }
    ]
  },
  "content-type-operations": {
    name: "content-type-operations",
    description: "Help with defining and managing content types and their fields",
    arguments: [
      {
        name: "task",
        description: "Specific task (create/update/delete/publish/field configuration)",
        required: false
      },
      {
        name: "details",
        description: "Additional context about field types or validations",
        required: false
      }
    ]
  },
  "ai-actions-overview": {
    name: "ai-actions-overview",
    description: "Comprehensive overview of AI Actions in Contentful",
    arguments: []
  },
  "ai-actions-create": {
    name: "ai-actions-create",
    description: "Guide for creating and configuring AI Actions in Contentful",
    arguments: [
      {
        name: "useCase",
        description: "Purpose of the AI Action you want to create",
        required: true
      },
      {
        name: "modelType",
        description: "AI model type (e.g., gpt-4, claude-3-opus)",
        required: false
      }
    ]
  },
  "ai-actions-variables": {
    name: "ai-actions-variables",
    description: "Explanation of variable types and configuration for AI Actions",
    arguments: [
      {
        name: "variableType",
        description: "Type of variable (Text, Reference, StandardInput, etc)",
        required: false
      }
    ]
  },
  "ai-actions-invoke": {
    name: "ai-actions-invoke",
    description: "Help with invoking AI Actions and processing results",
    arguments: [
      {
        name: "actionId",
        description: "ID of the AI Action (if known)",
        required: false
      },
      {
        name: "details",
        description: "Additional context about your invocation requirements",
        required: false
      }
    ]
  },
  "bulk-operations": {
    name: "bulk-operations",
    description: "Guidance on performing actions on multiple entities simultaneously",
    arguments: [
      {
        name: "operation",
        description: "Bulk operation type (publish/unpublish/validate)",
        required: false
      },
      {
        name: "entityType",
        description: "Type of entities to process (entries/assets)",
        required: false
      },
      {
        name: "details",
        description: "Additional context about operation requirements",
        required: false
      }
    ]
  },
  "space-environment-management": {
    name: "space-environment-management",
    description: "Help with managing spaces, environments, and deployment workflows",
    arguments: [
      {
        name: "task",
        description: "Specific task (create/list/manage environments/aliases)",
        required: false
      },
      {
        name: "entity",
        description: "Entity type (space/environment)",
        required: false
      },
      {
        name: "details",
        description: "Additional context about workflow requirements",
        required: false
      }
    ]
  },
  "mcp-tool-usage": {
    name: "mcp-tool-usage",
    description: "Instructions for using Contentful MCP tools effectively",
    arguments: [
      {
        name: "toolName",
        description: "Specific tool name (e.g., invoke_ai_action, create_entry)",
        required: false
      }
    ]
  }
};
```

--------------------------------------------------------------------------------
/codecompanion-workspace.json:
--------------------------------------------------------------------------------

```json
{
  "name": "Contentful MCP",
  "version": "1.0.0",
  "system_prompt": "Contentful MCP is a TypeScript-based Model Context Protocol (MCP) implementation for interacting with Contentful's Content Management API, particularly focusing on AI Actions integration. The project follows a modular architecture with handlers for different entity types (entries, assets, content types, AI actions), and includes utility functions for generating schemas and validating inputs.",
  "vars": {
    "typescript_path": "src/types",
    "handlers_path": "src/handlers",
    "utils_path": "src/utils",
    "config_path": "src/config"
  },
  "groups": [
    {
      "name": "Core",
      "system_prompt": "I've grouped core files together into a group called \"${group_name}\". These files represent the main entry points and configuration for the Contentful MCP project. They handle client setup, API connectivity, and server initialization.\n\nBelow are the relevant files which we will be discussing:\n\n${group_files}",
      "data": ["index", "ai-actions-client", "client"]
    },
    {
      "name": "Handlers",
      "system_prompt": "I've grouped handler files together into a group called \"${group_name}\". These files contain the implementation of various API handlers for different entity types in Contentful, providing CRUD operations and specialized functions.\n\nBelow are the relevant files which we will be discussing:\n\n${group_files}",
      "data": [
        "entry-handlers",
        "asset-handlers",
        "content-type-handlers",
        "ai-action-handlers",
        "bulk-action-handlers",
        "space-handlers"
      ]
    },
    {
      "name": "Types",
      "system_prompt": "I've grouped type definition files together into a group called \"${group_name}\". These files define the TypeScript interfaces and types used throughout the project, providing type safety and documentation.\n\nBelow are the relevant files which we will be discussing:\n\n${group_files}",
      "data": ["ai-actions-types", "tools-types"]
    },
    {
      "name": "Utils",
      "system_prompt": "I've grouped utility files together into a group called \"${group_name}\". These files provide helper functions and utilities used across the project for common operations and data transformations.\n\nBelow are the relevant files which we will be discussing:\n\n${group_files}",
      "data": ["ai-action-tool-generator", "summarizer", "validation"]
    }
  ],
  "data": {
    "index": {
      "type": "file",
      "path": "src/index.ts",
      "description": "The main entry point for the Contentful MCP project where handlers are registered and the server is initialized."
    },
    "ai-actions-client": {
      "type": "file",
      "path": "${config_path}/ai-actions-client.ts",
      "description": "Configuration for the AI Actions client used to interact with Contentful's AI Actions API, with specialized authentication and endpoint handling."
    },
    "client": {
      "type": "file",
      "path": "${config_path}/client.ts",
      "description": "Configuration for the main Contentful client used to interact with the Contentful API, handling authentication and base configuration."
    },
    "entry-handlers": {
      "type": "file",
      "path": "${handlers_path}/entry-handlers.ts",
      "description": "Handlers for CRUD operations and other actions on Contentful entries, including bulk publishing and unpublishing functionality."
    },
    "asset-handlers": {
      "type": "file",
      "path": "${handlers_path}/asset-handlers.ts",
      "description": "Handlers for CRUD operations and other actions on Contentful assets, including upload, update and publishing."
    },
    "content-type-handlers": {
      "type": "file",
      "path": "${handlers_path}/content-type-handlers.ts",
      "description": "Handlers for CRUD operations and other actions on Contentful content types, defining the structure of entries."
    },
    "ai-action-handlers": {
      "type": "file",
      "path": "${handlers_path}/ai-action-handlers.ts",
      "description": "Handlers for CRUD operations and other actions on Contentful AI Actions, including invocation and result retrieval."
    },
    "bulk-action-handlers": {
      "type": "file",
      "path": "${handlers_path}/bulk-action-handlers.ts",
      "description": "Handlers for bulk operations on Contentful entities, optimizing performance for operations on multiple items."
    },
    "space-handlers": {
      "type": "file",
      "path": "${handlers_path}/space-handlers.ts",
      "description": "Handlers for operations on Contentful spaces, providing top-level organization functionality."
    },
    "ai-actions-types": {
      "type": "file",
      "path": "${typescript_path}/ai-actions.ts",
      "description": "Type definitions for AI Actions entities and operations, including configuration, templates, and variables schemas."
    },
    "tools-types": {
      "type": "file",
      "path": "${typescript_path}/tools.ts",
      "description": "Type definitions for the MCP tools used in the project, defining the interface between the model and Contentful."
    },
    "ai-action-tool-generator": {
      "type": "file",
      "path": "${utils_path}/ai-action-tool-generator.ts",
      "description": "Utility for generating tool configurations for AI Actions, mapping parameters and creating JSON schemas."
    },
    "summarizer": {
      "type": "file",
      "path": "${utils_path}/summarizer.ts",
      "description": "Utility for summarizing content and responses to provide concise information to the model."
    },
    "validation": {
      "type": "file",
      "path": "${utils_path}/validation.ts",
      "description": "Utility for validating input data and responses to ensure data integrity and proper formatting."
    }
  }
}
```

--------------------------------------------------------------------------------
/src/handlers/asset-handlers.ts:
--------------------------------------------------------------------------------

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CreateAssetProps } from "contentful-management"
import { getContentfulClient } from "../config/client.js"

type BaseAssetParams = {
  spaceId: string
  environmentId: string
  assetId: string
}

const formatResponse = (data: any) => ({
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
})

const getCurrentAsset = async (params: BaseAssetParams) => {
  const contentfulClient = await getContentfulClient()
  return contentfulClient.asset.get(params)
}

import { summarizeData } from "../utils/summarizer.js"

export const assetHandlers = {
  listAssets: async (args: {
    spaceId: string
    environmentId: string
    limit: number
    skip: number
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      query: {
        limit: Math.min(args.limit || 3, 3),
        skip: args.skip || 0,
      },
    }

    const contentfulClient = await getContentfulClient()
    const assets = await contentfulClient.asset.getMany(params)

    const summarized = summarizeData(assets, {
      maxItems: 3,
      remainingMessage: "To see more assets, please ask me to retrieve the next page.",
    })

    return formatResponse(summarized)
  },
  uploadAsset: async (args: {
    spaceId: string
    environmentId: string
    title: string
    description?: string
    file: {
      fileName: string
      contentType: string
      upload?: string
    }
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
    }

    const assetProps: CreateAssetProps = {
      fields: {
        title: { "en-US": args.title },
        description: args.description ? { "en-US": args.description } : undefined,
        file: { "en-US": args.file },
      },
    }

    const contentfulClient = await getContentfulClient()
    const asset = await contentfulClient.asset.create(params, assetProps)

    const processedAsset = await contentfulClient.asset.processForAllLocales(
      params,
      {
        sys: asset.sys,
        fields: asset.fields,
      },
      {},
    )

    return formatResponse(processedAsset)
  },

  getAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      assetId: args.assetId,
    }

    const contentfulClient = await getContentfulClient()
    const asset = await contentfulClient.asset.get(params)
    return formatResponse(asset)
  },

  updateAsset: async (args: {
    spaceId: string
    environmentId: string
    assetId: string
    title?: string
    description?: string
    file?: {
      fileName: string
      contentType: string
      upload?: string
    }
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      assetId: args.assetId,
    }
    const currentAsset = await getCurrentAsset(params)

    const fields: Record<string, any> = {}
    if (args.title) fields.title = { "en-US": args.title }
    if (args.description) fields.description = { "en-US": args.description }
    if (args.file) fields.file = { "en-US": args.file }
    const updateParams = {
      fields: {
        title: args.title ? { "en-US": args.title } : currentAsset.fields.title,
        description: args.description
          ? { "en-US": args.description }
          : currentAsset.fields.description,
        file: args.file ? { "en-US": args.file } : currentAsset.fields.file,
      },
      sys: currentAsset.sys,
    }

    const contentfulClient = await getContentfulClient()
    const asset = await contentfulClient.asset.update(params, updateParams)

    return formatResponse(asset)
  },

  deleteAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      assetId: args.assetId,
    }
    const currentAsset = await getCurrentAsset(params)

    const contentfulClient = await getContentfulClient()
    await contentfulClient.asset.delete({
      ...params,
      version: currentAsset.sys.version,
    })

    return formatResponse({
      message: `Asset ${args.assetId} deleted successfully`,
    })
  },

  publishAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      assetId: args.assetId,
    }
    const currentAsset = await getCurrentAsset(params)

    const contentfulClient = await getContentfulClient()
    const asset = await contentfulClient.asset.publish(params, {
      sys: currentAsset.sys,
      fields: currentAsset.fields,
    })

    return formatResponse(asset)
  },

  unpublishAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      assetId: args.assetId,
    }
    const currentAsset = await getCurrentAsset(params)

    const contentfulClient = await getContentfulClient()
    const asset = await contentfulClient.asset.unpublish({
      ...params,
      version: currentAsset.sys.version,
    })

    return formatResponse(asset)
  },
}

```

--------------------------------------------------------------------------------
/src/handlers/content-type-handlers.ts:
--------------------------------------------------------------------------------

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getContentfulClient } from "../config/client.js"
import { ContentTypeProps, CreateContentTypeProps } from "contentful-management"
import { toCamelCase } from "../utils/to-camel-case.js"

export const contentTypeHandlers = {
  listContentTypes: async (args: { spaceId: string; environmentId: string }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
    }

    const contentfulClient = await getContentfulClient()
    const contentTypes = await contentfulClient.contentType.getMany(params)
    return {
      content: [{ type: "text", text: JSON.stringify(contentTypes, null, 2) }],
    }
  },
  getContentType: async (args: {
    spaceId: string
    environmentId: string
    contentTypeId: string
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      contentTypeId: args.contentTypeId,
    }

    const contentfulClient = await getContentfulClient()
    const contentType = await contentfulClient.contentType.get(params)
    return {
      content: [{ type: "text", text: JSON.stringify(contentType, null, 2) }],
    }
  },

  createContentType: async (args: {
    spaceId: string
    environmentId: string
    name: string
    fields: any[]
    description?: string
    displayField?: string
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      contentTypeId: toCamelCase(args.name),
      spaceId,
      environmentId,
    }

    const contentTypeProps: CreateContentTypeProps = {
      name: args.name,
      fields: args.fields,
      description: args.description || "",
      displayField: args.displayField || args.fields[0]?.id || "",
    }

    const contentfulClient = await getContentfulClient()
    const contentType = await contentfulClient.contentType.createWithId(params, contentTypeProps)

    return {
      content: [{ type: "text", text: JSON.stringify(contentType, null, 2) }],
    }
  },

  updateContentType: async (args: {
    spaceId: string
    environmentId: string
    contentTypeId: string
    name?: string
    fields?: any[]
    description?: string
    displayField?: string
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      contentTypeId: args.contentTypeId,
    }

    const contentfulClient = await getContentfulClient()
    const currentContentType = await contentfulClient.contentType.get(params)

    // Use the new fields if provided, otherwise keep existing fields
    const fields = args.fields || currentContentType.fields

    // If fields are provided, ensure we're not removing any required field metadata
    // This creates a map of existing fields by ID for easier lookup
    if (args.fields) {
      const existingFieldsMap = currentContentType.fields.reduce((acc: Record<string, any>, field: any) => {
        acc[field.id] = field
        return acc
      }, {})

      // Ensure each field has all required metadata
      fields.forEach((field: any) => {
        const existingField = existingFieldsMap[field.id]
        if (existingField) {
          // If this is an existing field, ensure we preserve any metadata not explicitly changed
          // This prevents losing validations, linkType, etc.
          field.validations = field.validations || existingField.validations

          // Preserve required flag if not explicitly set
          if (field.required === undefined && existingField.required !== undefined) {
            field.required = existingField.required
          }

          if (field.type === 'Link' && !field.linkType && existingField.linkType) {
            field.linkType = existingField.linkType
          }

          if (field.type === 'Array' && !field.items && existingField.items) {
            field.items = existingField.items
          }
        }
      })
    }

    const contentTypeProps: ContentTypeProps = {
      name: args.name || currentContentType.name,
      fields: fields,
      description: args.description || currentContentType.description || "",
      displayField: args.displayField || currentContentType.displayField || "",
      sys: currentContentType.sys,
    }

    const contentType = await contentfulClient.contentType.update(params, contentTypeProps)
    return {
      content: [{ type: "text", text: JSON.stringify(contentType, null, 2) }],
    }
  },

  deleteContentType: async (args: {
    spaceId: string
    environmentId: string
    contentTypeId: string
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      contentTypeId: args.contentTypeId,
    }

    const contentfulClient = await getContentfulClient()
    await contentfulClient.contentType.delete(params)
    return {
      content: [
        {
          type: "text",
          text: `Content type ${args.contentTypeId} deleted successfully`,
        },
      ],
    }
  },

  publishContentType: async (args: {
    spaceId: string
    environmentId: string
    contentTypeId: string
  }) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const params = {
      spaceId,
      environmentId,
      contentTypeId: args.contentTypeId,
    }

    const contentfulClient = await getContentfulClient()
    const contentType = await contentfulClient.contentType.get(params)
    await contentfulClient.contentType.publish(params, contentType)

    return {
      content: [
        {
          type: "text",
          text: `Content type ${args.contentTypeId} published successfully`,
        },
      ],
    }
  },
}

```

--------------------------------------------------------------------------------
/src/prompts/promptHandlers/aiActions.ts:
--------------------------------------------------------------------------------

```typescript
import { GetPromptResult } from "@modelcontextprotocol/sdk/types";
import { generateVariableTypeContent } from "../generateVariableTypeContent";
import { handleAiActionsInvoke } from "../ai-actions-invoke";
import { handleAiActionsOverview } from "../ai-actions-overview";

/**
 * Handler for AI Actions create prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleAiActionsCreate(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful AI Actions specialist. I can help you design, create, and configure AI Actions that integrate AI capabilities into your content workflows. I understand templates, variables, conditions, and model settings.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I want to create an AI Action for this use case: ${args?.useCase || "my Contentful project"}. ${args?.modelType ? `I'm planning to use the ${args?.modelType} model.` : "Please recommend appropriate model types."} Guide me through the creation process, including template design, variable configuration, and testing.`,
        },
      },
      {
        role: "assistant",
        content: {
          type: "text",
          text: `# Creating an AI Action for ${args?.useCase || "your Contentful project"}

I'll guide you through creating an effective AI Action for your use case. Here's a step-by-step approach:

## 1. Define your AI Action

Let's start by clearly defining what your AI Action will do:

- **Name**: Choose a descriptive name that reflects the action's purpose
- **Description**: Write a clear description of what the AI Action does and when to use it

## 2. Choose the right model

${args?.modelType ? `You've mentioned using the ${args?.modelType} model, which is a good choice for this use case.` : `For your use case, consider these models:
- **GPT-4** (gpt-4): Best for complex reasoning and high-quality content generation
- **Claude 3 Opus** (claude-3-opus): Excellent for nuanced understanding and creative content
- **Claude 3 Sonnet** (claude-3-5-sonnet): Good balance of quality and performance
- **GPT-3.5 Turbo** (gpt-3.5-turbo): Fastest option for simpler tasks`}

**Temperature setting**: 
- Lower (0.0-0.3): More consistent, predictable outputs
- Medium (0.4-0.7): Balance of creativity and consistency
- Higher (0.8-1.0): More creative, varied outputs

## 3. Design your template

The template is the prompt that will be sent to the AI model, with placeholders for variables. Here are some best practices:

- Start with clear instructions about the task
- Define the desired tone, style, and format
- Include context about how the output will be used
- Use variable placeholders with double curly braces: {{variable_name}}

Example template structure:

\`\`\`
You are helping create content for a Contentful entry.

TASK: [Clear description of what to generate]

CONTEXT: The content will be used for {{purpose}}.

TONE: {{tone}}

BASED ON THIS INFORMATION: {{input_content}}

GENERATE: [Specific output instructions]
\`\`\`

## 4. Define your variables

For each placeholder in your template, you'll need to define a variable:

### Common variable types:

1. **StandardInput**: For primary text input
2. **Text**: For simple text fields
3. **FreeFormInput**: For custom text input
4. **StringOptionsList**: For selecting from predefined options
5. **Reference**: For linking to other Contentful entries
6. **MediaReference**: For linking to assets (images, videos, etc.)
7. **Locale**: For specifying language/region

For each variable, define:
- ID: Internal identifier
- Name: User-friendly display name
- Description: Clear explanation of what the variable is for
- Type: One of the types above
- Configuration: Type-specific settings (e.g., option values for StringOptionsList)

## 5. Implementation steps

To create this AI Action using the MCP tools:

1. Use the **create_ai_action** tool with these parameters:
   - spaceId, environmentId
   - name, description
   - instruction (containing template and variables)
   - configuration (model type and temperature)

2. Test your AI Action:
   - Add test cases with sample values
   - Verify outputs match expectations

3. Publish your AI Action:
   - Use the **publish_ai_action** tool to make it available to content editors

Would you like me to help you structure a specific template and variables for your use case?`,
        },
      },
    ],
  };
}

/**
 * Handler for AI Actions variables prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleAiActionsVariables(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful AI Actions variables expert. I can explain how to configure variables for AI Actions, including different types, configurations, and best practices for different scenarios.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `${args?.variableType ? `Explain how to use and configure the ${args?.variableType} variable type in AI Actions.` : "Explain the different variable types available in AI Actions, their use cases, and how to configure them effectively."} Include examples and best practices for template integration.`,
        },
      },
      {
        role: "assistant",
        content: {
          type: "text",
          text: `# AI Action Variables${args?.variableType ? `: ${args?.variableType} Type` : " Overview"}

${generateVariableTypeContent(args?.variableType)}`,
        },
      },
    ],
  };
}

/**
 * Export all AI Actions related handlers
 */
export const aiActionsHandlers = {
  "ai-actions-overview": () => handleAiActionsOverview(),
  "ai-actions-create": (args?: Record<string, string>) => handleAiActionsCreate(args),
  "ai-actions-variables": (args?: Record<string, string>) => handleAiActionsVariables(args),
  "ai-actions-invoke": (args?: Record<string, string>) => handleAiActionsInvoke(args?.actionId, args?.details),
};
```

--------------------------------------------------------------------------------
/src/transports/sse.ts:
--------------------------------------------------------------------------------

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"
import { randomUUID } from "crypto"
import type { Request, Response } from "express"

/**
 * Interface for the SSE transport session
 */
interface SSESession {
  id: string
  response: Response
  server: Server
  isClosed: boolean
  lastEventId?: string
  heartbeatInterval?: NodeJS.Timeout
}

/**
 * Custom Transport class for SSE
 */
class SSEServerTransport {
  private session: SSESession

  // Callbacks for the transport
  onclose?: () => void
  onerror?: (error: Error) => void
  onmessage?: (message: JSONRPCMessage) => void

  constructor(session: SSESession) {
    this.session = session
  }

  async start(): Promise<void> {
    // Auto-heartbeat every 30 seconds to keep connection alive
    this.session.heartbeatInterval = setInterval(() => {
      if (!this.session.isClosed) {
        try {
          this.session.response.write(":heartbeat\n\n")
        } catch (error) {
          // Connection may be closed, clean up
          this.clearHeartbeat()
          SSETransport.closeSession(this.session.id)
        }
      } else {
        this.clearHeartbeat()
      }
    }, 30000)

    // Handle client disconnection
    this.session.response.on("close", () => {
      this.clearHeartbeat()
      SSETransport.closeSession(this.session.id)
    })

    // Send initial connection established event
    this.session.response.write(`event: connected\n`)
    this.session.response.write(`data: ${JSON.stringify({ sessionId: this.session.id })}\n\n`)
  }

  async send(message: JSONRPCMessage): Promise<void> {
    // Send message to client
    if (!this.session.isClosed) {
      try {
        const data = JSON.stringify(message)
        this.session.response.write(`id: ${message.id || "notification"}\n`)
        this.session.response.write(`data: ${data}\n\n`)
      } catch (error) {
        console.error(`Error sending SSE message for session ${this.session.id}:`, error)
      }
    }
  }

  async close(): Promise<void> {
    this.clearHeartbeat()
    SSETransport.closeSession(this.session.id)
  }

  private clearHeartbeat(): void {
    if (this.session.heartbeatInterval) {
      clearInterval(this.session.heartbeatInterval)
      this.session.heartbeatInterval = undefined
    }
  }
}

/**
 * Class to handle server-sent events (SSE) transport for the MCP server
 */
export class SSETransport {
  // Session store for managing active connections
  private static sessions: Record<string, SSESession> = {}

  /**
   * Handle an incoming SSE connection request
   *
   * @param req Express request object
   * @param res Express response object
   * @returns Session ID for the established connection
   */
  public static async handleConnection(req: Request, res: Response): Promise<string> {
    // Generate a unique session ID
    const sessionId = randomUUID()

    // Set SSE headers
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
      "X-Accel-Buffering": "no", // For Nginx compatibility
    })

    // Create a new server instance for this connection
    const server = new Server(
      {
        name: "contentful-mcp-server",
        version: "1.0.0",
      },
      {
        capabilities: {
          tools: {},
          prompts: {},
        },
      },
    )

    // Store the session
    const session: SSESession = {
      id: sessionId,
      response: res,
      server,
      isClosed: false,
      lastEventId: req.headers["last-event-id"] as string | undefined,
    }

    this.sessions[sessionId] = session

    // Create a transport for this session
    const transport = new SSEServerTransport(session)

    // Connect the transport to the server
    await server.connect(transport)

    // Return the session ID
    return sessionId
  }

  /**
   * Handle an incoming message for a session
   *
   * @param req Express request object
   * @param res Express response object
   * @param sessionId Session ID for the connection
   * @param message JSON-RPC message
   */
  public static async handleMessage(
    req: Request,
    res: Response,
    sessionId: string,
    message: JSONRPCMessage
  ): Promise<void> {
    const session = this.sessions[sessionId]

    if (!session || session.isClosed) {
      res.status(404).json({
        jsonrpc: "2.0",
        error: {
          code: -32000,
          message: "Session not found or closed",
        },
        id: message.id || null,
      })
      return
    }

    try {
      // Get the transport from the server
      // @ts-expect-error - Accessing transport property
      const transport = session.server.transport as SSEServerTransport

      // Pass the message to the transport's onmessage handler
      if (transport && transport.onmessage) {
        transport.onmessage(message)
      }

      // Send a success response
      res.status(200).json({
        jsonrpc: "2.0",
        result: { success: true },
        id: message.id || null,
      })
    } catch (error) {
      console.error(`Error handling message for session ${sessionId}:`, error)
      res.status(500).json({
        jsonrpc: "2.0",
        error: {
          code: -32603,
          message: `Error processing message: ${error instanceof Error ? error.message : String(error)}`,
        },
        id: message.id || null,
      })
    }
  }

  /**
   * Close a session
   *
   * @param sessionId Session ID to close
   */
  public static closeSession(sessionId: string): void {
    const session = this.sessions[sessionId]

    if (session && !session.isClosed) {
      session.isClosed = true

      try {
        // Clear heartbeat interval if it exists
        if (session.heartbeatInterval) {
          clearInterval(session.heartbeatInterval)
          session.heartbeatInterval = undefined
        }

        // End the response
        session.response.end()
      } catch (error) {
        console.error(`Error closing session ${sessionId}:`, error)
      } finally {
        // Delete the session
        delete this.sessions[sessionId]
      }
    }
  }

  /**
   * Get a session by ID
   *
   * @param sessionId Session ID
   * @returns Session object or undefined if not found
   */
  public static getSession(sessionId: string): SSESession | undefined {
    return this.sessions[sessionId]
  }

  /**
   * Get all active sessions
   *
   * @returns Array of session objects
   */
  public static getAllSessions(): SSESession[] {
    return Object.values(this.sessions)
  }
}
```

--------------------------------------------------------------------------------
/src/handlers/comment-handlers.ts:
--------------------------------------------------------------------------------

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getContentfulClient } from "../config/client.js"

export const commentHandlers = {
  getComments: async (args: {
    spaceId: string
    environmentId: string
    entryId: string
    bodyFormat?: "plain-text" | "rich-text"
    status?: "active" | "resolved" | "all"
    limit?: number
    skip?: number
  }) => {
    const spaceId =
      process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
        ? process.env.SPACE_ID
        : args.spaceId
    const environmentId =
      process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
        ? process.env.ENVIRONMENT_ID
        : args.environmentId
    const { entryId, bodyFormat = "plain-text", status = "active", limit = 10, skip = 0 } = args

    const baseParams = {
      spaceId,
      environmentId,
      entryId,
    }

    const contentfulClient = await getContentfulClient()

    // Build query based on status filter
    const query: { status?: "active" | "resolved" } = {}
    if (status !== "all") {
      query.status = status
    }

    // Handle different bodyFormat types separately due to TypeScript overloads
    const comments =
      bodyFormat === "rich-text"
        ? await contentfulClient.comment.getMany({
            ...baseParams,
            bodyFormat: "rich-text" as const,
            query,
          })
        : await contentfulClient.comment.getMany({
            ...baseParams,
            bodyFormat: "plain-text" as const,
            query,
          })

    // Apply manual pagination since Contentful Comments API doesn't support it
    const startIndex = skip
    const endIndex = skip + limit
    const paginatedItems = comments.items.slice(startIndex, endIndex)

    const paginatedResult = {
      items: paginatedItems,
      total: comments.total,
      showing: paginatedItems.length,
      remaining: Math.max(0, comments.total - endIndex),
      skip: endIndex < comments.total ? endIndex : undefined,
      message:
        endIndex < comments.total
          ? "To see more comments, use skip parameter with the provided skip value."
          : undefined,
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(paginatedResult, null, 2),
        },
      ],
    }
  },

  createComment: async (args: {
    spaceId: string
    environmentId: string
    entryId: string
    body: string
    status?: "active"
    parent?: string
  }) => {
    const spaceId =
      process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
        ? process.env.SPACE_ID
        : args.spaceId
    const environmentId =
      process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
        ? process.env.ENVIRONMENT_ID
        : args.environmentId
    const { entryId, body, parent } = args

    const baseParams = {
      spaceId,
      environmentId,
      entryId,
      // Add parentCommentId to baseParams when parent is provided
      ...(parent && { parentCommentId: parent }),
    }

    const contentfulClient = await getContentfulClient()

    // Simple comment data object (no parent in body)
    const commentData = {
      body,
      status: "active" as const,
    }

    const comment = await contentfulClient.comment.create(baseParams, commentData)

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(comment, null, 2),
        },
      ],
    }
  },

  getSingleComment: async (args: {
    spaceId: string
    environmentId: string
    entryId: string
    commentId: string
    bodyFormat?: "plain-text" | "rich-text"
  }) => {
    const spaceId =
      process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
        ? process.env.SPACE_ID
        : args.spaceId
    const environmentId =
      process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
        ? process.env.ENVIRONMENT_ID
        : args.environmentId
    const { entryId, commentId, bodyFormat = "plain-text" } = args

    const baseParams = {
      spaceId,
      environmentId,
      entryId,
      commentId,
    }

    const contentfulClient = await getContentfulClient()

    // Handle different bodyFormat types separately due to TypeScript overloads
    const comment =
      bodyFormat === "rich-text"
        ? await contentfulClient.comment.get({
            ...baseParams,
            bodyFormat: "rich-text" as const,
          })
        : await contentfulClient.comment.get({
            ...baseParams,
            bodyFormat: "plain-text" as const,
          })

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(comment, null, 2),
        },
      ],
    }
  },

  deleteComment: async (args: {
    spaceId: string
    environmentId: string
    entryId: string
    commentId: string
  }) => {
    const spaceId =
      process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
        ? process.env.SPACE_ID
        : args.spaceId
    const environmentId =
      process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
        ? process.env.ENVIRONMENT_ID
        : args.environmentId
    const { entryId, commentId } = args

    const baseParams = {
      spaceId,
      environmentId,
      entryId,
      commentId,
    }

    const contentfulClient = await getContentfulClient()

    // First get the comment to obtain its version
    const comment = await contentfulClient.comment.get({
      ...baseParams,
      bodyFormat: "plain-text" as const,
    })

    // Now delete with the version
    await contentfulClient.comment.delete({
      ...baseParams,
      version: comment.sys.version,
    })

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(
            {
              success: true,
              message: `Successfully deleted comment ${commentId} from entry ${entryId}`,
            },
            null,
            2,
          ),
        },
      ],
    }
  },

  updateComment: async (args: {
    spaceId: string
    environmentId: string
    entryId: string
    commentId: string
    body?: string
    status?: "active" | "resolved"
    bodyFormat?: "plain-text" | "rich-text"
  }) => {
    const spaceId =
      process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
        ? process.env.SPACE_ID
        : args.spaceId
    const environmentId =
      process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
        ? process.env.ENVIRONMENT_ID
        : args.environmentId
    const { entryId, commentId, body, status, bodyFormat = "plain-text" } = args

    const baseParams = {
      spaceId,
      environmentId,
      entryId,
      commentId,
    }

    // Build update data object with only provided fields
    const updateData: { body?: string; status?: "active" | "resolved" } = {}
    if (body !== undefined) updateData.body = body
    if (status !== undefined) updateData.status = status

    const contentfulClient = await getContentfulClient()

    // First get the comment to obtain its version
    const existingComment =
      bodyFormat === "rich-text"
        ? await contentfulClient.comment.get({
            ...baseParams,
            bodyFormat: "rich-text" as const,
          })
        : await contentfulClient.comment.get({
            ...baseParams,
            bodyFormat: "plain-text" as const,
          })

    // Update with the version
    const comment = await contentfulClient.comment.update(baseParams, {
      ...updateData,
      version: existingComment.sys.version,
    })

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(comment, null, 2),
        },
      ],
    }
  },
}

```

--------------------------------------------------------------------------------
/test/unit/content-type-handler-merge.test.ts:
--------------------------------------------------------------------------------

```typescript
import { expect, vi, describe, it, beforeEach } from "vitest"
import { contentTypeHandlers } from "../../src/handlers/content-type-handlers.js"

// Define constants
const TEST_CONTENT_TYPE_ID = "test-content-type-id"
const TEST_SPACE_ID = "test-space-id"
const TEST_ENV_ID = "master"

// Mock the Contentful client for testing the merge logic
vi.mock("../../src/config/client.js", () => {
  return {
    getContentfulClient: vi.fn().mockImplementation(() => {
      // Create mock content type inside the function implementation
      const mockContentType = {
        sys: { id: TEST_CONTENT_TYPE_ID, version: 1 },
        name: "Original Content Type",
        description: "Original description",
        displayField: "title",
        fields: [
          {
            id: "title",
            name: "Title",
            type: "Text",
            required: true,
            validations: [{ size: { max: 100 } }]
          },
          {
            id: "description",
            name: "Description",
            type: "Text",
            required: false
          },
          {
            id: "image",
            name: "Image",
            type: "Link",
            linkType: "Asset",
            required: false
          },
          {
            id: "tags",
            name: "Tags",
            type: "Array",
            items: {
              type: "Symbol"
            }
          }
        ]
      }

      return {
        contentType: {
          get: vi.fn().mockResolvedValue(mockContentType),
          update: vi.fn().mockImplementation((params, contentTypeProps) => {
            // Return a merged content type that simulates the updated fields
            return Promise.resolve({
              sys: { id: params.contentTypeId, version: 2 },
              name: contentTypeProps.name,
              description: contentTypeProps.description,
              displayField: contentTypeProps.displayField,
              fields: contentTypeProps.fields
            })
          })
        }
      }
    })
  }
})

describe("Content Type Handler Merge Logic", () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it("should use existing name when name is not provided", async () => {
    // Setup - update without providing name
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      contentTypeId: TEST_CONTENT_TYPE_ID,
      fields: [
        {
          id: "title",
          name: "Title",
          type: "Text",
          required: true,
          validations: [{ size: { max: 100 } }]
        },
        {
          id: "description",
          name: "Description",
          type: "Text",
          required: false
        },
        {
          id: "image",
          name: "Image",
          type: "Link",
          linkType: "Asset",
          required: false
        },
        {
          id: "tags",
          name: "Tags",
          type: "Array",
          items: {
            type: "Symbol"
          }
        }
      ],
      description: "Updated description"
    }

    // Execute
    const result = await contentTypeHandlers.updateContentType(updateData)
    
    // Parse the result
    const updatedContentType = JSON.parse(result.content[0].text)
    
    // Assert - should keep the original name but update description
    expect(updatedContentType.name).toEqual("Original Content Type")
    expect(updatedContentType.description).toEqual("Updated description")
  })

  it("should preserve field metadata when updating fields", async () => {
    // Setup - update with simplified field definition that's missing metadata
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      contentTypeId: TEST_CONTENT_TYPE_ID,
      fields: [
        {
          id: "title",
          name: "New Title Name",
          type: "Text"
          // Intentionally missing required, validations, etc.
        },
        {
          id: "image",
          name: "Updated Image",
          type: "Link"
          // Missing linkType
        },
        {
          id: "tags",
          name: "Updated Tags",
          type: "Array"
          // Missing items definition
        }
      ]
    }

    // Execute
    const result = await contentTypeHandlers.updateContentType(updateData)
    
    // Parse the result
    const updatedContentType = JSON.parse(result.content[0].text)
    
    // Assert - fields should be updated but metadata should be preserved
    const titleField = updatedContentType.fields.find(f => f.id === "title")
    expect(titleField.name).toEqual("New Title Name") // Updated
    expect(titleField.required).toEqual(true) // Preserved from original
    expect(titleField.validations).toEqual([{ size: { max: 100 } }]) // Preserved from original
    
    const imageField = updatedContentType.fields.find(f => f.id === "image")
    expect(imageField.name).toEqual("Updated Image") // Updated
    expect(imageField.linkType).toEqual("Asset") // Preserved from original
    
    const tagsField = updatedContentType.fields.find(f => f.id === "tags")
    expect(tagsField.name).toEqual("Updated Tags") // Updated
    expect(tagsField.items).toEqual({ type: "Symbol" }) // Preserved from original
  })

  it("should handle adding new fields", async () => {
    // Define the original and a new field
    const originalFields = [
      {
        id: "title",
        name: "Title",
        type: "Text",
        required: true,
        validations: [{ size: { max: 100 } }]
      },
      {
        id: "description",
        name: "Description",
        type: "Text",
        required: false
      },
      {
        id: "image",
        name: "Image",
        type: "Link",
        linkType: "Asset",
        required: false
      },
      {
        id: "tags",
        name: "Tags",
        type: "Array",
        items: {
          type: "Symbol"
        }
      }
    ]
    
    const newField = {
      id: "newField",
      name: "New Field",
      type: "Boolean",
      required: false
    }
    
    // Setup - add a new field
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      contentTypeId: TEST_CONTENT_TYPE_ID,
      fields: [...originalFields, newField]
    }

    // Execute
    const result = await contentTypeHandlers.updateContentType(updateData)
    
    // Parse the result
    const updatedContentType = JSON.parse(result.content[0].text)
    
    // Assert - should have all original fields plus the new one
    expect(updatedContentType.fields.length).toEqual(5) // 4 original + 1 new
    const addedField = updatedContentType.fields.find(f => f.id === "newField")
    expect(addedField).toEqual({
      id: "newField",
      name: "New Field",
      type: "Boolean",
      required: false
    })
  })

  it("should handle when no fields are provided", async () => {
    // Setup - update without providing fields, should use existing fields
    const updateData = {
      spaceId: TEST_SPACE_ID,
      environmentId: TEST_ENV_ID,
      contentTypeId: TEST_CONTENT_TYPE_ID,
      name: "Updated Content Type Name"
    }

    // Execute
    const result = await contentTypeHandlers.updateContentType(updateData)
    
    // Parse the result
    const updatedContentType = JSON.parse(result.content[0].text)
    
    // Assert - should use existing fields but updated name
    expect(updatedContentType.name).toEqual("Updated Content Type Name")
    expect(updatedContentType.fields.length).toEqual(4) // All 4 original fields should be preserved
    
    // Check that all original fields are preserved
    const titleField = updatedContentType.fields.find(f => f.id === "title")
    expect(titleField).toBeDefined()
    expect(titleField.name).toEqual("Title")
    
    const descriptionField = updatedContentType.fields.find(f => f.id === "description")
    expect(descriptionField).toBeDefined()
    expect(descriptionField.name).toEqual("Description")
  })
})
```

--------------------------------------------------------------------------------
/test/integration/streamable-http.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { StreamableHttpServer } from "../../src/transports/streamable-http.js"
import express from "express"
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"

// Mock the Server class
vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
  return {
    Server: vi.fn().mockImplementation(() => {
      return {
        connect: vi.fn().mockImplementation(async (transport) => {
          // Manually set callbacks for testing
          if (transport.start) {
            await transport.start()
          }
        }),
        setRequestHandler: vi.fn(),
      }
    })
  }
})

// Mock the StreamableHTTPServerTransport
vi.mock("@modelcontextprotocol/sdk/server/streamableHttp.js", () => {
  return {
    StreamableHTTPServerTransport: vi.fn().mockImplementation(({ sessionIdGenerator, onsessioninitialized }) => {
      const sessionId = sessionIdGenerator()
      onsessioninitialized(sessionId)
      
      return {
        sessionId,
        handleRequest: vi.fn().mockResolvedValue(undefined),
        onclose: undefined,
        start: vi.fn().mockResolvedValue(undefined),
        close: vi.fn().mockResolvedValue(undefined),
      }
    })
  }
})

describe("StreamableHTTP Server", () => {
  let app: express.Application
  
  beforeEach(() => {
    // Create a test app
    app = express()
    vi.clearAllMocks()
  })
  
  it("should set up correct routes", () => {
    // Create StreamableHTTP server instance that uses the test app
    const httpServer = new StreamableHttpServer({
      port: 0 // Use any available port for testing
    })
    
    // @ts-expect-error - Replace the app with our test app
    httpServer.app = app
    
    // Add spy on route configuration methods
    const appAllSpy = vi.spyOn(app, 'all')
    const appGetSpy = vi.spyOn(app, 'get')
    
    // Setup routes manually
    // @ts-expect-error - Access private method for testing
    httpServer.setupRoutes()

    // Verify that the expected routes were set up
    expect(appAllSpy).toHaveBeenCalledTimes(1) // /mcp

    // Express internally calls app.get with a function as the first argument for query parsing
    // We only care about the route paths, so filter to only include string paths
    const actualRoutes = appGetSpy.mock.calls
      .filter(call => typeof call[0] === 'string' && call[0].startsWith('/'))
    expect(actualRoutes.length).toBe(1) // Only /health is a real route
    
    // Check specific routes
    const mcpCallArgs = appAllSpy.mock.calls.find(call => call[0] === '/mcp')
    const healthCallArgs = appGetSpy.mock.calls.find(call => call[0] === '/health')
    
    expect(mcpCallArgs).toBeDefined()
    expect(healthCallArgs).toBeDefined()
  })
  
  it("should handle POST initialization requests correctly", async () => {
    // Create StreamableHTTP server
    const httpServer = new StreamableHttpServer({
      port: 0 // Use any available port for testing
    })
    
    // @ts-expect-error - Replace the app with our test app
    httpServer.app = app
    
    // @ts-expect-error - Access private method for testing
    httpServer.setupRoutes()
    
    // Mock request and response
    const req = {
      method: "POST",
      headers: {},
      body: {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: {
          protocolVersion: "2025-03-26",
          capabilities: {},
          clientInfo: {
            name: "test-client",
            version: "1.0.0",
          },
        },
      },
    } as any
    
    const res = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
      send: vi.fn(),
      writeHead: vi.fn(),
      headersSent: false,
    } as any
    
    // Get the route handler
    const routeHandler = app._router.stack
      .find((layer: any) => layer.route && layer.route.path === '/mcp')
      ?.route.stack[0].handle
    
    // Call the route handler
    await routeHandler(req, res)
    
    // Check that StreamableHTTPServerTransport was created
    expect(StreamableHTTPServerTransport).toHaveBeenCalledTimes(1)
    
    // Check that handleRequest was called with the initialization request
    const transportInstance = (StreamableHTTPServerTransport as any).mock.results[0].value
    expect(transportInstance.handleRequest).toHaveBeenCalledWith(req, res, req.body)
  })
  
  it("should reject POST requests without session ID or initialization", async () => {
    // Create StreamableHTTP server
    const httpServer = new StreamableHttpServer({
      port: 0 // Use any available port for testing
    })
    
    // @ts-expect-error - Replace the app with our test app
    httpServer.app = app
    
    // @ts-expect-error - Access private method for testing
    httpServer.setupRoutes()
    
    // Mock request and response
    const req = {
      method: "POST",
      headers: {},
      body: {
        jsonrpc: "2.0",
        method: "test",
        id: 1,
      },
    } as any
    
    const res = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
      headersSent: false,
    } as any
    
    // Get the route handler
    const routeHandler = app._router.stack
      .find((layer: any) => layer.route && layer.route.path === '/mcp')
      ?.route.stack[0].handle
    
    // Call the route handler
    await routeHandler(req, res)
    
    // Check that it rejected the request
    expect(res.status).toHaveBeenCalledWith(400)
    expect(res.json).toHaveBeenCalledWith({
      jsonrpc: "2.0",
      error: {
        code: -32000,
        message: "Bad Request: No valid session ID provided for non-initialize request",
      },
      id: null,
    })
  })
  
  it("should set up server handlers correctly", () => {
    // Create StreamableHTTP server
    const httpServer = new StreamableHttpServer({
      port: 0 // Use any available port for testing
    })
    
    // Create a mock server
    const mockServer = {
      setRequestHandler: vi.fn(),
    }
    
    // Call setupServerHandlers
    // @ts-expect-error - Access private method for testing
    httpServer.setupServerHandlers(mockServer as any)
    
    // Verify that setRequestHandler was called for all expected handlers
    expect(mockServer.setRequestHandler).toHaveBeenCalledTimes(4)
  })
  
  it("should start and stop the server", async () => {
    // Create StreamableHTTP server
    const httpServer = new StreamableHttpServer({
      port: 0 // Use any available port for testing
    })
    
    // Mock the app.listen method
    const listenMock = vi.fn().mockImplementation((port, callback) => {
      callback()
      return { close: vi.fn().mockImplementation((cb) => cb()) }
    })
    // @ts-expect-error - Replace the app.listen with our mock
    httpServer.app.listen = listenMock
    
    // Start the server
    await httpServer.start()
    
    // Verify that listen was called
    expect(listenMock).toHaveBeenCalledTimes(1)
    
    // Stop the server (with no active transports)
    await httpServer.stop()
    
    // Now test with an active transport
    // @ts-expect-error - Access private property for testing
    httpServer.transports = {
      "test-session": {
        close: vi.fn().mockResolvedValue(undefined),
      } as any,
    }
    
    // Stop the server (with an active transport)
    await httpServer.stop()
    
    // Verify that transport.close was called
    // @ts-expect-error - Access private property for testing
    expect(httpServer.transports["test-session"].close).toHaveBeenCalledTimes(1)
  })
  
  it("should get handler for valid tool names", () => {
    // Create StreamableHTTP server
    const httpServer = new StreamableHttpServer({
      port: 0 // Use any available port for testing
    })
    
    // Test getting a handler for a known tool
    // @ts-expect-error - Access private method for testing
    const handler = httpServer.getHandler("get_entry")
    
    // Verify that a handler was returned
    expect(handler).toBeDefined()
    
    // Test getting a handler for an unknown tool
    // @ts-expect-error - Access private method for testing
    const unknownHandler = httpServer.getHandler("unknown_tool")
    
    // Verify that no handler was returned
    expect(unknownHandler).toBeUndefined()
  })
})
```

--------------------------------------------------------------------------------
/src/handlers/bulk-action-handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { getContentfulClient } from "../config/client.js"

type BulkPublishParams = {
  spaceId: string
  environmentId: string
  entities: Array<{
    sys: {
      id: string
      type: "Entry" | "Asset"
    }
  }>
}

// Define the correct types for bulk action responses
interface BulkActionResponse {
  sys: {
    id: string
    status: string
  }
  succeeded?: Array<{
    sys: {
      id: string
      type: string
    }
  }>
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  error?: any
}

// Define the correct types for bulk action payloads
interface VersionedLink {
  sys: {
    type: "Link"
    linkType: "Entry" | "Asset"
    id: string
    version: number
  }
}

// Define a Collection type to match SDK expectations
interface Collection<T> {
  sys: {
    type: "Array"
  }
  items: T[]
}

type BulkUnpublishParams = BulkPublishParams

type BulkValidateParams = {
  spaceId: string
  environmentId: string
  entryIds: string[]
}

export const bulkActionHandlers = {
  bulkPublish: async (args: BulkPublishParams) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const contentfulClient = await getContentfulClient()

    // Get the current version of each entity
    const entityVersions: VersionedLink[] = await Promise.all(
      args.entities.map(async (entity) => {
        try {
          // Get the current version of the entity
          const currentEntity =
            entity.sys.type === "Entry"
              ? await contentfulClient.entry.get({
                  spaceId,
                  environmentId,
                  entryId: entity.sys.id,
                })
              : await contentfulClient.asset.get({
                  spaceId,
                  environmentId,
                  assetId: entity.sys.id,
                })

          // Explicitly create a VersionedLink with the correct type
          const versionedLink: VersionedLink = {
            sys: {
              type: "Link" as const,
              linkType: entity.sys.type as "Entry" | "Asset",
              id: entity.sys.id,
              version: currentEntity.sys.version,
            },
          }

          return versionedLink
        } catch (error) {
          console.error(`Error fetching entity ${entity.sys.id}: ${error}`)
          throw new Error(
            `Failed to get version for entity ${entity.sys.id}. All entities must have a version.`,
          )
        }
      }),
    )

    // Create the collection object with the correct structure
    const entitiesCollection: Collection<VersionedLink> = {
      sys: {
        type: "Array",
      },
      items: entityVersions,
    }

    // Create the bulk action
    const bulkAction = await contentfulClient.bulkAction.publish(
      {
        spaceId,
        environmentId,
      },
      {
        entities: entitiesCollection,
      },
    )

    // Wait for the bulk action to complete
    let action = (await contentfulClient.bulkAction.get({
      spaceId,
      environmentId,
      bulkActionId: bulkAction.sys.id,
    })) as unknown as BulkActionResponse

    while (action.sys.status === "inProgress" || action.sys.status === "created") {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      action = (await contentfulClient.bulkAction.get({
        spaceId,
        environmentId,
        bulkActionId: bulkAction.sys.id,
      })) as unknown as BulkActionResponse
    }

    return {
      content: [
        {
          type: "text",
          text: `Bulk publish completed with status: ${action.sys.status}. ${
            action.sys.status === "failed"
              ? `Error: ${JSON.stringify(action.error)}`
              : `Successfully processed ${action.succeeded?.length || 0} items.`
          }`,
        },
      ],
    }
  },

  bulkUnpublish: async (args: BulkUnpublishParams) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const contentfulClient = await getContentfulClient()

    // Get the current version of each entity
    const entityVersions: VersionedLink[] = await Promise.all(
      args.entities.map(async (entity) => {
        try {
          // Get the current version of the entity
          const currentEntity =
            entity.sys.type === "Entry"
              ? await contentfulClient.entry.get({
                  spaceId,
                  environmentId,
                  entryId: entity.sys.id,
                })
              : await contentfulClient.asset.get({
                  spaceId,
                  environmentId,
                  assetId: entity.sys.id,
                })

          // Explicitly create a VersionedLink with the correct type
          const versionedLink: VersionedLink = {
            sys: {
              type: "Link" as const,
              linkType: entity.sys.type as "Entry" | "Asset",
              id: entity.sys.id,
              version: currentEntity.sys.version,
            },
          }

          return versionedLink
        } catch (error) {
          console.error(`Error fetching entity ${entity.sys.id}: ${error}`)
          throw new Error(
            `Failed to get version for entity ${entity.sys.id}. All entities must have a version.`,
          )
        }
      }),
    )

    // Create the collection object with the correct structure
    const entitiesCollection: Collection<VersionedLink> = {
      sys: {
        type: "Array",
      },
      items: entityVersions,
    }

    // Create the bulk action
    const bulkAction = await contentfulClient.bulkAction.unpublish(
      {
        spaceId,
        environmentId,
      },
      {
        entities: entitiesCollection,
      },
    )

    // Wait for the bulk action to complete
    let action = (await contentfulClient.bulkAction.get({
      spaceId,
      environmentId,
      bulkActionId: bulkAction.sys.id,
    })) as unknown as BulkActionResponse

    while (action.sys.status === "inProgress" || action.sys.status === "created") {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      action = (await contentfulClient.bulkAction.get({
        spaceId,
        environmentId,
        bulkActionId: bulkAction.sys.id,
      })) as unknown as BulkActionResponse
    }

    return {
      content: [
        {
          type: "text",
          text: `Bulk unpublish completed with status: ${action.sys.status}. ${
            action.sys.status === "failed"
              ? `Error: ${JSON.stringify(action.error)}`
              : `Successfully processed ${action.succeeded?.length || 0} items.`
          }`,
        },
      ],
    }
  },

  bulkValidate: async (args: BulkValidateParams) => {
    const spaceId = process.env.SPACE_ID || args.spaceId
    const environmentId = process.env.ENVIRONMENT_ID || args.environmentId

    const contentfulClient = await getContentfulClient()

    // Get the current version of each entry
    const entityVersions: VersionedLink[] = await Promise.all(
      args.entryIds.map(async (id) => {
        try {
          // Get the current version of the entry
          const currentEntry = await contentfulClient.entry.get({
            spaceId,
            environmentId,
            entryId: id,
          })

          // Explicitly create a VersionedLink with the correct type
          const versionedLink: VersionedLink = {
            sys: {
              type: "Link" as const,
              linkType: "Entry",
              id,
              version: currentEntry.sys.version,
            },
          }

          return versionedLink
        } catch (error) {
          console.error(`Error fetching entry ${id}: ${error}`)
          throw new Error(`Failed to get version for entry ${id}. All entries must have a version.`)
        }
      }),
    )

    // Create the collection object with the correct structure
    const entitiesCollection: Collection<VersionedLink> = {
      sys: {
        type: "Array",
      },
      items: entityVersions,
    }

    // Create the bulk action
    const bulkAction = await contentfulClient.bulkAction.validate(
      {
        spaceId,
        environmentId,
      },
      {
        entities: entitiesCollection,
      },
    )

    // Wait for the bulk action to complete
    let action = (await contentfulClient.bulkAction.get({
      spaceId,
      environmentId,
      bulkActionId: bulkAction.sys.id,
    })) as unknown as BulkActionResponse

    while (action.sys.status === "inProgress" || action.sys.status === "created") {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      action = (await contentfulClient.bulkAction.get({
        spaceId,
        environmentId,
        bulkActionId: bulkAction.sys.id,
      })) as unknown as BulkActionResponse
    }

    return {
      content: [
        {
          type: "text",
          text: `Bulk validation completed with status: ${action.sys.status}. ${
            action.sys.status === "failed"
              ? `Error: ${JSON.stringify(action.error)}`
              : `Successfully validated ${action.succeeded?.length || 0} entries.`
          }`,
        },
      ],
    }
  },
}

```

--------------------------------------------------------------------------------
/test/integration/entry-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { expect, vi } from "vitest"
import { entryHandlers } from "../../src/handlers/entry-handlers.js"
import { server } from "../msw-setup.js"

// Mock Contentful client bulkAction methods
const mockBulkActionPublish = vi.fn().mockResolvedValue({ 
  sys: { id: "mock-bulk-action-id", status: "created" } 
})

const mockBulkActionGet = vi.fn().mockResolvedValue({
  sys: { id: "mock-bulk-action-id", status: "succeeded" },
  succeeded: [
    { sys: { id: "entry-id-1", type: "Entry" }},
    { sys: { id: "entry-id-2", type: "Entry" }}
  ]
})

// Mock the contentful client for testing bulk operations
vi.mock("../../src/config/client.js", async (importOriginal) => {
  const originalModule = await importOriginal();
  
  // Create a mock function that will be used for the content client
  const getContentfulClient = vi.fn();
  
  // Store the original function so we can call it if needed
  const originalGetClient = originalModule.getContentfulClient;
  
  // Set up the mock function to return either the original or our mocked version
  getContentfulClient.mockImplementation(async () => {
    // Create our mock client
    const mockClient = {
      entry: {
        get: vi.fn().mockImplementation((params) => {
          // Special handling for our bulk test entries
          if (params.entryId === "entry-id-1" || params.entryId === "entry-id-2") {
            return Promise.resolve({
              sys: { id: params.entryId, version: 1 },
              fields: { title: { "en-US": "Test Entry" } }
            });
          }
          
          // Otherwise, call the original implementation
          return originalGetClient().then(client => client.entry.get(params));
        }),
        // Mock the other methods by passing through
        getMany: (...args) => originalGetClient().then(client => client.entry.getMany(...args)),
        create: (...args) => originalGetClient().then(client => client.entry.create(...args)),
        update: (...args) => originalGetClient().then(client => client.entry.update(...args)),
        delete: (...args) => originalGetClient().then(client => client.entry.delete(...args)),
        publish: (...args) => originalGetClient().then(client => client.entry.publish(...args)),
        unpublish: (...args) => originalGetClient().then(client => client.entry.unpublish(...args)),
      },
      bulkAction: {
        publish: mockBulkActionPublish,
        unpublish: mockBulkActionPublish, // Using same mock for simplicity
        get: mockBulkActionGet
      }
    };
    
    return mockClient;
  });
  
  return {
    ...originalModule,
    getContentfulClient
  };
});

describe("Entry Handlers Integration Tests", () => {
  // Start MSW Server before tests
  beforeAll(() => server.listen())
  afterEach(() => server.resetHandlers())
  afterAll(() => server.close())

  const testSpaceId = "test-space-id"
  const testEntryId = "test-entry-id"
  const testContentTypeId = "test-content-type-id"

  describe("searchEntries", () => {
    it("should search all entries", async () => {
      const result = await entryHandlers.searchEntries({
        spaceId: testSpaceId,
        query: {
          content_type: testContentTypeId,
        },
      })

      expect(result).to.have.property("content").that.is.an("array")
      expect(result.content).to.have.lengthOf(1)

      const entries = JSON.parse(result.content[0].text)
      expect(entries.items).to.be.an("array")
      expect(entries.items[0]).to.have.nested.property("sys.id", "test-entry-id")
      expect(entries.items[0]).to.have.nested.property("fields.title.en-US", "Test Entry")
    })
  })

  describe("getEntry", () => {
    it("should get details of a specific entry", async () => {
      const result = await entryHandlers.getEntry({
        spaceId: testSpaceId,
        entryId: testEntryId,
      })

      expect(result).to.have.property("content")
      const entry = JSON.parse(result.content[0].text)
      expect(entry.sys.id).to.equal(testEntryId)
      expect(entry).to.have.nested.property("fields.title.en-US", "Test Entry")
    })

    it("should throw error for invalid entry ID", async () => {
      try {
        await entryHandlers.getEntry({
          spaceId: testSpaceId,
          entryId: "invalid-entry-id",
        })
        expect.fail("Should have thrown an error")
      } catch (error) {
        expect(error).to.exist
      }
    })
  })

  describe("createEntry", () => {
    it("should create a new entry", async () => {
      const entryData = {
        spaceId: testSpaceId,
        contentTypeId: testContentTypeId,
        fields: {
          title: { "en-US": "New Entry" },
          description: { "en-US": "New Description" },
        },
      }

      const result = await entryHandlers.createEntry(entryData)

      expect(result).to.have.property("content")
      const entry = JSON.parse(result.content[0].text)
      expect(entry).to.have.nested.property("sys.id", "new-entry-id")
      expect(entry).to.have.nested.property("fields.title.en-US", "New Entry")
    })
  })

  describe("updateEntry", () => {
    it("should update an existing entry", async () => {
      const updateData = {
        spaceId: testSpaceId,
        entryId: testEntryId,
        fields: {
          title: { "en-US": "Updated Entry" },
          description: { "en-US": "Updated Description" },
        },
      }

      const result = await entryHandlers.updateEntry(updateData)

      expect(result).to.have.property("content")
      const entry = JSON.parse(result.content[0].text)
      expect(entry.sys.id).to.equal(testEntryId)
      expect(entry).to.have.nested.property("fields.title.en-US", "Updated Entry")
    })
  })

  describe("deleteEntry", () => {
    it("should delete an entry", async () => {
      const result = await entryHandlers.deleteEntry({
        spaceId: testSpaceId,
        entryId: testEntryId,
      })

      expect(result).to.have.property("content")
      expect(result.content[0].text).to.include("deleted successfully")
    })

    it("should throw error when deleting non-existent entry", async () => {
      try {
        await entryHandlers.deleteEntry({
          spaceId: testSpaceId,
          entryId: "non-existent-id",
        })
        expect.fail("Should have thrown an error")
      } catch (error) {
        expect(error).to.exist
      }
    })
  })

  describe("publishEntry", () => {
    it("should publish a single entry", async () => {
      const result = await entryHandlers.publishEntry({
        spaceId: testSpaceId,
        entryId: testEntryId,
      })

      expect(result).to.have.property("content")
      const entry = JSON.parse(result.content[0].text)
      expect(entry.sys.publishedVersion).to.exist
    })

    it("should publish multiple entries using bulk publish", async () => {
      // Clear previous calls to the mocks
      mockBulkActionPublish.mockClear()
      mockBulkActionGet.mockClear()

      const result = await entryHandlers.publishEntry({
        spaceId: testSpaceId,
        entryId: ["entry-id-1", "entry-id-2"],
      })

      // Verify the bulk publish was called
      expect(mockBulkActionPublish).toHaveBeenCalled()
      
      // Verify the payload structure
      const callArgs = mockBulkActionPublish.mock.calls[0][1]
      expect(callArgs).to.have.property('entities')
      expect(callArgs.entities.sys.type).to.equal('Array')
      expect(callArgs.entities.items).to.have.length(2)
      expect(callArgs.entities.items[0].sys.id).to.equal('entry-id-1')
      expect(callArgs.entities.items[1].sys.id).to.equal('entry-id-2')

      // Verify the response
      expect(result).to.have.property("content")
      expect(result.content[0].text).to.include("Bulk publish completed")
      expect(result.content[0].text).to.include("Successfully processed")
    })
  })

  describe("unpublishEntry", () => {
    it("should unpublish a single entry", async () => {
      const result = await entryHandlers.unpublishEntry({
        spaceId: testSpaceId,
        entryId: testEntryId,
      })

      expect(result).to.have.property("content")
      const entry = JSON.parse(result.content[0].text)
      expect(entry.sys.publishedVersion).to.not.exist
    })

    it("should unpublish multiple entries using bulk unpublish", async () => {
      // Clear previous calls to the mocks
      mockBulkActionPublish.mockClear()
      mockBulkActionGet.mockClear()

      const result = await entryHandlers.unpublishEntry({
        spaceId: testSpaceId,
        entryId: ["entry-id-1", "entry-id-2"],
      })

      // Verify the bulk unpublish was called
      expect(mockBulkActionPublish).toHaveBeenCalled()
      
      // Verify the payload structure
      const callArgs = mockBulkActionPublish.mock.calls[0][1]
      expect(callArgs).to.have.property('entities')
      expect(callArgs.entities.sys.type).to.equal('Array')
      expect(callArgs.entities.items).to.have.length(2)
      expect(callArgs.entities.items[0].sys.id).to.equal('entry-id-1')
      expect(callArgs.entities.items[1].sys.id).to.equal('entry-id-2')

      // Verify the response
      expect(result).to.have.property("content")
      expect(result.content[0].text).to.include("Bulk unpublish completed")
      expect(result.content[0].text).to.include("Successfully processed")
    })
  })
})

```

--------------------------------------------------------------------------------
/src/config/ai-actions-client.ts:
--------------------------------------------------------------------------------

```typescript
import { getContentfulClient } from "./client"
import type {
  AiActionEntity,
  AiActionEntityCollection,
  AiActionInvocation,
  AiActionInvocationType,
  AiActionSchemaParsed,
  StatusFilter,
} from "../types/ai-actions"

// Alpha header required for AI Actions (temporary - will be removed in 2-3 weeks)
// TODO: Remove alpha header after 2-3 weeks (around May 2025) when it's no longer required
const ALPHA_HEADER_NAME = "X-Contentful-Enable-Alpha-Feature"
const ALPHA_HEADER_VALUE = "ai-service"

/**
 * Add alpha header to request options
 * @param options Request options
 * @returns Options with alpha header added
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function withAlphaHeader(options: any = {}): any {
  const headers = options.headers || {}
  return {
    ...options,
    headers: {
      ...headers,
      [ALPHA_HEADER_NAME]: ALPHA_HEADER_VALUE,
    },
  }
}

/**
 * Extract data from response, handling both direct response and response.data formats
 * @param response API response from contentful-management client
 * @returns Extracted data
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractResponseData<T>(response: any): T {
  // If we have a response but no data property, check if the response itself is the data
  if (response && !response.data && typeof response === "object") {
    // For collections (AI Action listing)
    if ("items" in response && "sys" in response && response.sys.type === "Array") {
      return response as T
    }

    // For single entities (AI Action retrieval)
    if ("sys" in response) {
      return response as T
    }
  }

  // Default to the data property
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (response as any).data as T
}

/**
 * Parameters for AI Action API operations
 */

// List AI Actions
export interface ListAiActionsParams {
  spaceId: string
  environmentId?: string
  limit?: number
  skip?: number
  status?: StatusFilter
}

// Get AI Action
export interface GetAiActionParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
}

// Create AI Action
export interface CreateAiActionParams {
  spaceId: string
  environmentId?: string
  actionData: AiActionSchemaParsed
}

// Update AI Action
export interface UpdateAiActionParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
  version: number
  actionData: AiActionSchemaParsed
}

// Delete AI Action
export interface DeleteAiActionParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
  version: number
}

// Publish AI Action
export interface PublishAiActionParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
  version: number
}

// Unpublish AI Action
export interface UnpublishAiActionParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
}

// Invoke AI Action
export interface InvokeAiActionParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
  invocationData: AiActionInvocationType
}

// Get AI Action invocation
export interface GetAiActionInvocationParams {
  spaceId: string
  environmentId?: string
  aiActionId: string
  invocationId: string
}

/**
 * AI Actions client for interacting with the Contentful API
 */
export const aiActionsClient = {
  /**
   * List AI Actions in a space
   */
  async listAiActions({
    spaceId,
    environmentId = "master",
    limit = 100,
    skip = 0,
    status,
  }: ListAiActionsParams): Promise<AiActionEntityCollection> {
    const client = await getContentfulClient()

    // Build the URL for listing AI actions
    let url = `/spaces/${spaceId}`

    // Add environment if specified (API uses space-level endpoint for AI Actions)
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += "/ai/actions"
    const queryParams = new URLSearchParams()

    queryParams.append("limit", limit.toString())
    queryParams.append("skip", skip.toString())
    if (status) {
      queryParams.append("status", status)
    }

    const queryString = queryParams.toString()
    if (queryString) {
      url += `?${queryString}`
    }

    const response = await client.raw.get(url, withAlphaHeader())
    return extractResponseData<AiActionEntityCollection>(response)
  },

  /**
   * Get a specific AI Action
   */
  async getAiAction({
    spaceId,
    environmentId = "master",
    aiActionId,
  }: GetAiActionParams): Promise<AiActionEntity> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`

    // Add environment if specified
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += `/ai/actions/${aiActionId}`

    const response = await client.raw.get(url, withAlphaHeader())
    return extractResponseData<AiActionEntity>(response)
  },

  /**
   * Create a new AI Action
   */
  async createAiAction({
    spaceId,
    environmentId = "master",
    actionData,
  }: CreateAiActionParams): Promise<AiActionEntity> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`

    // Add environment if specified
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += `/ai/actions`

    const response = await client.raw.post(url, actionData, withAlphaHeader())
    return extractResponseData<AiActionEntity>(response)
  },

  /**
   * Update an existing AI Action
   */
  async updateAiAction({
    spaceId,
    environmentId = "master",
    aiActionId,
    version,
    actionData,
  }: UpdateAiActionParams): Promise<AiActionEntity> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`

    // Add environment if specified
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += `/ai/actions/${aiActionId}`
    const headers = {
      "X-Contentful-Version": version.toString(),
    }

    const response = await client.raw.put(url, actionData, withAlphaHeader({ headers }))
    return extractResponseData<AiActionEntity>(response)
  },

  /**
   * Delete an AI Action
   */
  async deleteAiAction({
    spaceId,
    environmentId = "master",
    aiActionId,
    version,
  }: DeleteAiActionParams): Promise<void> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`

    // Add environment if specified
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += `/ai/actions/${aiActionId}`
    const headers = {
      "X-Contentful-Version": version.toString(),
    }

    await client.raw.delete(url, withAlphaHeader({ headers }))
  },

  /**
   * Publish an AI Action
   */
  async publishAiAction({
    spaceId,
    environmentId = "master",
    aiActionId,
    version,
  }: PublishAiActionParams): Promise<AiActionEntity> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`

    // Add environment if specified
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += `/ai/actions/${aiActionId}/published`
    const headers = {
      "X-Contentful-Version": version.toString(),
    }

    const response = await client.raw.put(url, {}, withAlphaHeader({ headers }))
    return extractResponseData<AiActionEntity>(response)
  },

  /**
   * Unpublish an AI Action
   */
  async unpublishAiAction({
    spaceId,
    environmentId = "master",
    aiActionId,
  }: UnpublishAiActionParams): Promise<AiActionEntity> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`

    // Add environment if specified
    if (environmentId) {
      url += `/environments/${environmentId}`
    }

    url += `/ai/actions/${aiActionId}/published`
    const response = await client.raw.delete(url, withAlphaHeader())
    return extractResponseData<AiActionEntity>(response)
  },

  /**
   * Invoke an AI Action
   */
  async invokeAiAction({
    spaceId,
    environmentId = "master",
    aiActionId,
    invocationData,
  }: InvokeAiActionParams): Promise<AiActionInvocation> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`
    if (environmentId) {
      url += `/environments/${environmentId}`
    }
    url += `/ai/actions/${aiActionId}/invoke`

    const headers = {
      "X-Contentful-Include-Invocation-Metadata": "true",
    }

    // Debug log the invocation data before sending
    console.error(`AI Action invocation request to ${url}:`, JSON.stringify(invocationData))

    const response = await client.raw.post(url, invocationData, withAlphaHeader({ headers }))
    return extractResponseData<AiActionInvocation>(response)
  },

  /**
   * Get an AI Action invocation result
   */
  async getAiActionInvocation({
    spaceId,
    environmentId = "master",
    aiActionId,
    invocationId,
  }: GetAiActionInvocationParams): Promise<AiActionInvocation> {
    const client = await getContentfulClient()

    let url = `/spaces/${spaceId}`
    if (environmentId) {
      url += `/environments/${environmentId}`
    }
    url += `/ai/actions/${aiActionId}/invocations/${invocationId}`

    const headers = {
      "X-Contentful-Include-Invocation-Metadata": "true",
    }

    const response = await client.raw.get(url, withAlphaHeader({ headers }))
    return extractResponseData<AiActionInvocation>(response)
  },

  /**
   * Poll an AI Action invocation until it is complete or fails
   * @param params The invocation parameters
   * @param maxAttempts Maximum number of polling attempts
   * @param initialDelay Initial delay in ms between polling attempts
   * @param maxDelay Maximum delay in ms between polling attempts
   */
  async pollInvocation(
    params: GetAiActionInvocationParams,
    maxAttempts = 10,
    initialDelay = 1000,
    maxDelay = 5000,
  ): Promise<AiActionInvocation> {
    let attempts = 0
    let delay = initialDelay

    while (attempts < maxAttempts) {
      const invocation = await this.getAiActionInvocation(params)

      if (
        invocation.sys.status === "COMPLETED" ||
        invocation.sys.status === "FAILED" ||
        invocation.sys.status === "CANCELLED"
      ) {
        return invocation
      }

      // Wait with exponential backoff
      await new Promise((resolve) => setTimeout(resolve, delay))
      delay = Math.min(delay * 1.5, maxDelay)
      attempts++
    }

    throw new Error(`AI Action invocation polling exceeded maximum attempts (${maxAttempts})`)
  },
}

```

--------------------------------------------------------------------------------
/src/prompts/promptHandlers/contentful.ts:
--------------------------------------------------------------------------------

```typescript
import { GetPromptResult } from "@modelcontextprotocol/sdk/types";

/**
 * Handler for API concepts prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleApiConcepts(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful API specialist. I can explain core concepts like Spaces, Environments, Content Types, Entries, Assets, and how they relate to each other in the Contentful Content Management and Delivery APIs. I'll include practical examples and best practices.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `Please explain the Contentful concept: ${args?.concept}. Include how it's used in real applications and any important API endpoints related to it.`,
        },
      },
    ],
  };
}

/**
 * Handler for content modeling guide prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleContentModelingGuide(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful content modeling consultant. I can help you design efficient, scalable content structures that support your specific business needs. I understand content types, fields, validations, references, and best practices for headless CMS architecture.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `Help me design a content model for this use case: ${args?.useCase}. Please describe your project requirements, target platforms, and any specific content relationships you need to maintain.`,
        },
      },
    ],
  };
}

/**
 * Handler for API operation help prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleApiOperationHelp(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful API implementation guide. I can explain CMA, CDA, CPA, GraphQL, and Images API operations with code examples. I'm familiar with pagination, filtering, localization, and API rate limits across different SDKs and environments.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `Explain how to perform a ${args?.operation} operation on a ${args?.resourceType} in Contentful. If relevant, please specify your programming language or environment so I can provide appropriate SDK examples.`,
        },
      },
    ],
  };
}

/**
 * Handler for space identification prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleSpaceIdentification(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful space navigator. I can help you identify and select the right space for your operations. I understand space organization, access controls, and how to locate specific spaces in your Contentful organization.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need to identify the correct Contentful space for this operation: ${args?.operation}. Please guide me through the process of finding and selecting the appropriate space.`,
        },
      },
    ],
  };
}

/**
 * Handler for entry management prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleEntryManagement(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful entry management expert. I can help you with CRUD operations, publishing workflows, and bulk actions for content entries. I understand entry versioning, localization, references, and validation processes in the Contentful API.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need assistance with ${args?.task || "managing"} entries in my Contentful space. ${args?.details || "Please guide me through the process, available tools, and provide code examples if applicable."}`,
        },
      },
    ],
  };
}

/**
 * Handler for asset management prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleAssetManagement(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful asset management specialist. I can help you upload, process, and publish digital assets like images, videos, and documents. I understand asset processing, image transformations, and media optimization in Contentful.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need help with ${args?.task || "managing"} assets in Contentful. ${args?.details || "Please explain the process, available tools, and potential challenges."}`,
        },
      },
    ],
  };
}

/**
 * Handler for content type operations prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleContentTypeOperations(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful content type specialist. I can help you create, modify, and manage content types and their fields. I understand field validations, appearances, required fields, editor interfaces, and content type migrations.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need assistance with ${args?.task || "defining"} content types in Contentful. ${args?.details || "Please guide me on best practices, available tools, and implementation details."}`,
        },
      },
    ],
  };
}

/**
 * Handler for bulk operations prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleBulkOperations(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful bulk operations specialist. I can help you efficiently perform actions on multiple entries or assets simultaneously. I understand bulk publishing, unpublishing, validation, and how to track the status of bulk operations.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need to ${args?.operation || "perform a bulk operation"} on multiple ${args?.entityType || "entities"} in Contentful. ${args?.details || "Please explain the process, available tools, and any limitations I should be aware of."}`,
        },
      },
    ],
  };
}

/**
 * Handler for space/environment management prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleSpaceEnvironmentManagement(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful space and environment expert. I can help you manage spaces, create and configure environments, and understand deployment workflows. I'm familiar with environment aliases, branching strategies, and content staging practices.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `I need assistance with ${args?.task || "managing"} ${args?.entity || "spaces and environments"} in Contentful. ${args?.details || "Please guide me through the process, available tools, and best practices."}`,
        },
      },
    ],
  };
}

/**
 * Handler for MCP tool usage prompt
 * @param args Optional arguments for the prompt
 * @returns Prompt result with messages
 */
export function handleMcpToolUsage(args?: Record<string, string>): GetPromptResult {
  return {
    messages: [
      {
        role: "assistant",
        content: {
          type: "text",
          text: "I'm your Contentful MCP tool specialist. I can explain how to use the Model Context Protocol tools available in this integration to efficiently work with Contentful from your AI assistant.",
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text: `${args?.toolName ? `Explain how to use the ${args?.toolName} tool in the Contentful MCP integration.` : "Please provide an overview of the available tools in the Contentful MCP integration and how to use them effectively."} Include parameter explanations, example use cases, and common patterns.`,
        },
      },
    ],
  };
}

/**
 * Export all general Contentful handlers
 */
export const contentfulHandlers = {
  "explain-api-concepts": (args?: Record<string, string>) => handleApiConcepts(args),
  "content-modeling-guide": (args?: Record<string, string>) => handleContentModelingGuide(args),
  "api-operation-help": (args?: Record<string, string>) => handleApiOperationHelp(args),
  "space-identification": (args?: Record<string, string>) => handleSpaceIdentification(args),
  "entry-management": (args?: Record<string, string>) => handleEntryManagement(args),
  "asset-management": (args?: Record<string, string>) => handleAssetManagement(args),
  "content-type-operations": (args?: Record<string, string>) => handleContentTypeOperations(args),
  "bulk-operations": (args?: Record<string, string>) => handleBulkOperations(args),
  "space-environment-management": (args?: Record<string, string>) => handleSpaceEnvironmentManagement(args),
  "mcp-tool-usage": (args?: Record<string, string>) => handleMcpToolUsage(args),
};
```

--------------------------------------------------------------------------------
/test/unit/ai-action-tool-generator.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest"
import {
  generateAiActionToolSchema,
  mapVariablesToInvocationFormat,
  AiActionToolContext,
} from "../../src/utils/ai-action-tool-generator"
import type { AiActionEntity } from "../../src/types/ai-actions"

describe("AI Action Tool Generator", () => {
  const mockTextAction: AiActionEntity = {
    sys: {
      id: "text-action",
      type: "AiAction",
      createdAt: "2023-01-01T00:00:00Z",
      updatedAt: "2023-01-02T00:00:00Z",
      version: 1,
      space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
      createdBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
      updatedBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
    },
    name: "Text Action",
    description: "An action with text variables",
    instruction: {
      template: "Template with {{var1}} and {{var2}}",
      variables: [
        { id: "var1", type: "Text", name: "Variable 1", description: "First variable" },
        { id: "var2", type: "FreeFormInput", name: "Variable 2" },
      ],
    },
    configuration: {
      modelType: "gpt-4",
      modelTemperature: 0.5,
    },
  }

  const mockOptionsAction: AiActionEntity = {
    sys: {
      id: "options-action",
      type: "AiAction",
      createdAt: "2023-01-01T00:00:00Z",
      updatedAt: "2023-01-02T00:00:00Z",
      version: 1,
      space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
      createdBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
      updatedBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
    },
    name: "Options Action",
    description: "An action with options",
    instruction: {
      template: "Template with {{option}}",
      variables: [
        {
          id: "option",
          type: "StringOptionsList",
          name: "Option",
          configuration: {
            values: ["option1", "option2", "option3"],
            allowFreeFormInput: false,
          },
        },
      ],
    },
    configuration: {
      modelType: "gpt-4",
      modelTemperature: 0.5,
    },
  }

  const mockReferenceAction: AiActionEntity = {
    sys: {
      id: "reference-action",
      type: "AiAction",
      createdAt: "2023-01-01T00:00:00Z",
      updatedAt: "2023-01-02T00:00:00Z",
      version: 1,
      space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
      createdBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
      updatedBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
    },
    name: "Reference Action",
    description: "An action with references",
    instruction: {
      template: "Template with {{entry}} and {{asset}}",
      variables: [
        {
          id: "entry",
          type: "Reference",
          name: "Entry Reference",
          configuration: { allowedEntities: ["Entry"] },
        },
        { id: "asset", type: "MediaReference", name: "Asset Reference" },
      ],
    },
    configuration: {
      modelType: "gpt-4",
      modelTemperature: 0.5,
    },
  }

  it("should generate a tool schema for a text action", () => {
    const schema = generateAiActionToolSchema(mockTextAction)

    expect(schema.name).toBe("ai_action_text-action")
    expect(schema.description).toContain(
      "This action is called: Text Action, it's purpose: An action with text variables",
    )
    expect(schema.description).toContain(
      "This AI Action works on content entries and fields in Contentful",
    )

    // Check variables in schema - now using friendly names
    const inputSchema = schema.inputSchema
    expect(inputSchema.properties).toHaveProperty("variable_1") // snake_case of "Variable 1"
    expect(inputSchema.properties).toHaveProperty("variable_2") // snake_case of "Variable 2"
    expect(inputSchema.properties.variable_1.type).toBe("string")
    expect(inputSchema.properties.variable_1.description).toContain("First variable")

    // All variables should be required now
    expect(inputSchema.required).toContain("variable_1")
    expect(inputSchema.required).toContain("variable_2") // Now required
    expect(inputSchema.required).toContain("outputFormat") // outputFormat is also required
  })

  it("should generate a tool schema for an options action", () => {
    const schema = generateAiActionToolSchema(mockOptionsAction)

    expect(schema.name).toBe("ai_action_options-action")

    // Check options in schema
    const inputSchema = schema.inputSchema
    expect(inputSchema.properties).toHaveProperty("option") // Keeps original name since it's clean
    expect(inputSchema.properties.option.type).toBe("string")
    expect(inputSchema.properties.option.enum).toEqual(["option1", "option2", "option3"])

    // option should be required
    expect(inputSchema.required).toContain("option")
  })

  it("should generate a tool schema for a reference action", () => {
    const schema = generateAiActionToolSchema(mockReferenceAction)

    expect(schema.name).toBe("ai_action_reference-action")

    // Check reference variables in schema - now uses friendly names
    const inputSchema = schema.inputSchema
    expect(inputSchema.properties).toHaveProperty("entry_reference") // friendly name
    expect(inputSchema.properties).toHaveProperty("asset_reference") // friendly name
    expect(inputSchema.properties.entry_reference.type).toBe("string")
    expect(inputSchema.properties.asset_reference.type).toBe("string")
    expect(inputSchema.properties.entry_reference.description).toContain("Entry Reference")

    // References should be required
    expect(inputSchema.required).toContain("entry_reference")
    expect(inputSchema.required).toContain("asset_reference")
  })

  it("should map variables to invocation format", () => {
    const toolInput = {
      var1: "value1",
      var2: "value2",
      outputFormat: "Markdown",
    }

    const result = mapVariablesToInvocationFormat(mockTextAction, toolInput)

    expect(result.variables).toHaveLength(2)
    expect(result.variables[0]).toEqual({ id: "var1", value: "value1" })
    expect(result.variables[1]).toEqual({ id: "var2", value: "value2" })
    expect(result.outputFormat).toBe("Markdown")
  })

  it("should map reference variables to invocation format", () => {
    const toolInput = {
      entry: "entry123",
      asset: "asset456",
      outputFormat: "RichText",
    }

    const result = mapVariablesToInvocationFormat(mockReferenceAction, toolInput)

    expect(result.variables).toHaveLength(2)
    expect(result.variables[0]).toEqual({
      id: "entry",
      value: { entityType: "Entry", entityId: "entry123" },
    })
    expect(result.variables[1]).toEqual({
      id: "asset",
      value: { entityType: "Asset", entityId: "asset456" },
    })
    expect(result.outputFormat).toBe("RichText")
  })

  it("should manage AI Actions in the tool context", () => {
    const context = new AiActionToolContext("space1", "master")

    // Add actions
    context.addAiAction(mockTextAction)
    context.addAiAction(mockOptionsAction)

    // Get all actions
    const allActions = context.getAllAiActions()
    expect(allActions).toHaveLength(2)

    // Get specific action
    const action = context.getAiAction("text-action")
    expect(action).toBe(mockTextAction)

    // Generate all tool schemas
    const schemas = context.generateAllToolSchemas()
    expect(schemas).toHaveLength(2)
    expect(schemas[0].name).toBe("ai_action_text-action")
    expect(schemas[1].name).toBe("ai_action_options-action")

    // Remove an action
    context.removeAiAction("text-action")
    expect(context.getAllAiActions()).toHaveLength(1)

    // Clear cache
    context.clearCache()
    expect(context.getAllAiActions()).toHaveLength(0)
  })

  it("should generate invocation parameters", () => {
    const context = new AiActionToolContext("space1", "master")
    context.addAiAction(mockTextAction)

    const toolInput = {
      var1: "value1",
      var2: "value2",
      outputFormat: "PlainText",
      waitForCompletion: false,
    }

    const params = context.getInvocationParams("text-action", toolInput)

    expect(params).toEqual({
      spaceId: "space1",
      environmentId: "master",
      aiActionId: "text-action",
      outputFormat: "PlainText",
      variables: [
        { id: "var1", value: "value1" },
        { id: "var2", value: "value2" },
      ],
      waitForCompletion: false,
    })
  })

  it("should use friendly parameter names in tool schema and map them back for invocation", () => {
    // Create a mock AI Action with cryptic variable IDs
    const crypticAction: AiActionEntity = {
      sys: {
        id: "cryptic-action",
        type: "AiAction",
        createdAt: "2023-01-01T00:00:00Z",
        updatedAt: "2023-01-02T00:00:00Z",
        version: 1,
        space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
        createdBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
        updatedBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
      },
      name: "Cryptic Action",
      description: "An action with cryptic IDs",
      instruction: {
        template: "Template with {{x7yz12b}} and {{87abcde}}",
        variables: [
          { id: "x7yz12b", type: "Text", name: "First Variable", description: "Named text input" },
          { id: "87abcde", type: "StandardInput", description: "Unnamed standard input" },
        ],
      },
      configuration: {
        modelType: "gpt-4",
        modelTemperature: 0.5,
      },
    }

    // Generate tool schema
    const schema = generateAiActionToolSchema(crypticAction)

    // Check that friendly names are used in the schema
    const inputSchema = schema.inputSchema
    expect(inputSchema.properties).toHaveProperty("first_variable") // snake_case conversion of name
    expect(inputSchema.properties).toHaveProperty("input_text") // Standard naming for StandardInput
    expect(inputSchema.properties).not.toHaveProperty("x7yz12b") // Original ID not used
    expect(inputSchema.properties).not.toHaveProperty("87abcde") // Original ID not used

    // Test parameter translation
    const context = new AiActionToolContext("space1", "master")
    context.addAiAction(crypticAction)

    // Input using friendly names
    const toolInput = {
      first_variable: "value1",
      input_text: "value2",
      outputFormat: "Markdown",
    }

    const params = context.getInvocationParams("cryptic-action", toolInput)

    // Verify the parameters were translated to original IDs
    expect(params.variables).toEqual([
      { id: "x7yz12b", value: "value1" },
      { id: "87abcde", value: "value2" },
    ])
  })
})

```
Page 1/2FirstPrevNextLast