#
tokens: 47913/50000 47/64 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/ivo-toby/contentful-mcp?lines=true&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:
--------------------------------------------------------------------------------

```
1 | @contentful:registry=https://registry.npmjs.org
2 | 
```

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

```
1 | {
2 |   "printWidth": 100,
3 |   "semi": false,
4 |   "singleQuote": false
5 | }
6 | 
```

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

```
 1 | {
 2 |   "branches": ["main", "master"],
 3 |   "plugins": [
 4 |     "@semantic-release/commit-analyzer",
 5 |     "@semantic-release/release-notes-generator",
 6 |     "@semantic-release/npm",
 7 |     "@semantic-release/github"
 8 |   ]
 9 | }
10 | 
```

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

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 | 
110 | # Serverless directories
111 | .serverless/
112 | 
113 | # FuseBox cache
114 | .fusebox/
115 | 
116 | # DynamoDB Local files
117 | .dynamodb/
118 | 
119 | # TernJS port file
120 | .tern-port
121 | 
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 | 
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 | 
132 | build/
133 | 
134 | gcp-oauth.keys.json
135 | .*-server-credentials.json
136 | 
137 | # Byte-compiled / optimized / DLL files
138 | __pycache__/
139 | *.py[cod]
140 | *$py.class
141 | 
142 | # C extensions
143 | *.so
144 | 
145 | # Distribution / packaging
146 | .Python
147 | build/
148 | develop-eggs/
149 | dist/
150 | downloads/
151 | eggs/
152 | .eggs/
153 | lib/
154 | lib64/
155 | parts/
156 | sdist/
157 | var/
158 | wheels/
159 | share/python-wheels/
160 | *.egg-info/
161 | .installed.cfg
162 | *.egg
163 | MANIFEST
164 | 
165 | # PyInstaller
166 | #  Usually these files are written by a python script from a template
167 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
168 | *.manifest
169 | *.spec
170 | 
171 | # Installer logs
172 | pip-log.txt
173 | pip-delete-this-directory.txt
174 | 
175 | # Unit test / coverage reports
176 | htmlcov/
177 | .tox/
178 | .nox/
179 | .coverage
180 | .coverage.*
181 | .cache
182 | nosetests.xml
183 | coverage.xml
184 | *.cover
185 | *.py,cover
186 | .hypothesis/
187 | .pytest_cache/
188 | cover/
189 | 
190 | # Translations
191 | *.mo
192 | *.pot
193 | 
194 | # Django stuff:
195 | *.log
196 | local_settings.py
197 | db.sqlite3
198 | db.sqlite3-journal
199 | 
200 | # Flask stuff:
201 | instance/
202 | .webassets-cache
203 | 
204 | # Scrapy stuff:
205 | .scrapy
206 | 
207 | # Sphinx documentation
208 | docs/_build/
209 | 
210 | # PyBuilder
211 | .pybuilder/
212 | target/
213 | 
214 | # Jupyter Notebook
215 | .ipynb_checkpoints
216 | 
217 | # IPython
218 | profile_default/
219 | ipython_config.py
220 | 
221 | # pyenv
222 | #   For a library or package, you might want to ignore these files since the code is
223 | #   intended to run in multiple environments; otherwise, check them in:
224 | # .python-version
225 | 
226 | # pipenv
227 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
228 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
229 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
230 | #   install all needed dependencies.
231 | #Pipfile.lock
232 | 
233 | # poetry
234 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
235 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
236 | #   commonly ignored for libraries.
237 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
238 | #poetry.lock
239 | 
240 | # pdm
241 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
242 | #pdm.lock
243 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
244 | #   in version control.
245 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
246 | .pdm.toml
247 | .pdm-python
248 | .pdm-build/
249 | 
250 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
251 | __pypackages__/
252 | 
253 | # Celery stuff
254 | celerybeat-schedule
255 | celerybeat.pid
256 | 
257 | # SageMath parsed files
258 | *.sage.py
259 | 
260 | # Environments
261 | .env
262 | .venv
263 | env/
264 | venv/
265 | ENV/
266 | env.bak/
267 | venv.bak/
268 | 
269 | # Spyder project settings
270 | .spyderproject
271 | .spyproject
272 | 
273 | # Rope project settings
274 | .ropeproject
275 | 
276 | # mkdocs documentation
277 | /site
278 | 
279 | # mypy
280 | .mypy_cache/
281 | .dmypy.json
282 | dmypy.json
283 | 
284 | # Pyre type checker
285 | .pyre/
286 | 
287 | # pytype static type analyzer
288 | .pytype/
289 | 
290 | # Cython debug symbols
291 | cython_debug/
292 | 
293 | .DS_Store
294 | 
295 | # PyCharm
296 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
297 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
298 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
299 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
300 | #.idea/
301 | .aider*
302 | Cntfl-Readme.md
303 | 
304 | **/.claude/settings.local.json
305 | 
```

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

```markdown
  1 | <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"/>
  2 | 
  3 | # Contentful MCP Server
  4 | 
  5 | ## Notice
  6 | 
  7 | This is a community driven server! Contentful has released an official server which you can find [here](https://github.com/contentful/contentful-mcp-server)
  8 | 
  9 | [![smithery badge](https://smithery.ai/badge/@ivotoby/contentful-management-mcp-server)](https://smithery.ai/server/@ivotoby/contentful-management-mcp-server)
 10 | 
 11 | An MCP server implementation that integrates with Contentful's Content Management API, providing comprehensive content management capabilities.
 12 | 
 13 | - Please note \*; if you are not interested in the code, and just want to use this MCP in
 14 |   Claude Desktop (or any other tool that is able to use MCP servers) you don't have to
 15 |   clone this repo, you can just set it up in Claude desktop, refer to the section
 16 |   "Usage with Claude Desktop" for instructions on how to install it.
 17 | 
 18 | <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>
 19 | 
 20 | ## Features
 21 | 
 22 | - **Content Management**: Full CRUD operations for entries and assets
 23 | - **Comment Management**: Create, retrieve, and manage comments on entries with support for both plain-text and rich-text formats, including threaded conversations
 24 | - **Space Management**: Create, update, and manage spaces and environments
 25 | - **Content Types**: Manage content type definitions
 26 | - **Localization**: Support for multiple locales
 27 | - **Publishing**: Control content publishing workflow
 28 | - **Bulk Operations**: Execute bulk publishing, unpublishing, and validation across multiple entries and assets
 29 | - **Smart Pagination**: List operations return maximum 3 items per request to prevent context window overflow, with built-in pagination support
 30 | 
 31 | ## Pagination
 32 | 
 33 | 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:
 34 | 
 35 | - Total number of available items
 36 | - Current page of items (max 3)
 37 | - Number of remaining items
 38 | - Skip value for the next page
 39 | - Message prompting the LLM to offer retrieving more items
 40 | 
 41 | This pagination system allows the LLM to efficiently handle large datasets while maintaining context window limits.
 42 | 
 43 | ## Bulk Operations
 44 | 
 45 | The bulk operations feature provides efficient management of multiple content items simultaneously:
 46 | 
 47 | - **Asynchronous Processing**: Operations run asynchronously and provide status updates
 48 | - **Efficient Content Management**: Process multiple entries or assets in a single API call
 49 | - **Status Tracking**: Monitor progress with success and failure counts
 50 | - **Resource Optimization**: Reduce API calls and improve performance for batch operations
 51 | 
 52 | These bulk operation tools are ideal for content migrations, mass updates, or batch publishing workflows.
 53 | 
 54 | ## Tools
 55 | 
 56 | ### Entry Management
 57 | 
 58 | - **search_entries**: Search for entries using query parameters
 59 | - **create_entry**: Create new entries
 60 | - **get_entry**: Retrieve existing entries
 61 | - **update_entry**: Update entry fields
 62 | - **delete_entry**: Remove entries
 63 | - **publish_entry**: Publish entries
 64 | - **unpublish_entry**: Unpublish entries
 65 | 
 66 | ### Comment Management
 67 | 
 68 | - **get_comments**: Retrieve comments for an entry with filtering by status (active, resolved, all)
 69 | - **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
 70 | - **get_single_comment**: Retrieve a specific comment by its ID for an entry
 71 | - **delete_comment**: Delete a specific comment from an entry
 72 | - **update_comment**: Update existing comments with new body content or status changes
 73 | 
 74 | #### Threaded Comments
 75 | 
 76 | Comments support threading functionality to enable structured conversations and work around the 512-character limit:
 77 | 
 78 | - **Reply to Comments**: Use the `parent` parameter in `create_comment` to reply to an existing comment
 79 | - **Threaded Conversations**: Build conversation trees by replying to specific comments
 80 | - **Extended Discussions**: Work around the 512-character limit by creating threaded replies to continue longer messages
 81 | - **Conversation Context**: Maintain context in discussions by organizing related comments in threads
 82 | 
 83 | Example usage:
 84 | 
 85 | 1. Create a main comment: `create_comment` with `entryId`, `body`, and `status`
 86 | 2. Reply to that comment: `create_comment` with `entryId`, `body`, `status`, and `parent` (the ID of the comment you're replying to)
 87 | 3. Continue the thread: Reply to any comment in the thread by using its ID as the `parent`
 88 | 
 89 | ### Bulk Operations
 90 | 
 91 | - **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.
 92 | - **bulk_unpublish**: Unpublish multiple entries and assets in a single operation. Similar to bulk_publish but removes content from the delivery API.
 93 | - **bulk_validate**: Validate multiple entries for content consistency, references, and required fields. Returns validation results without modifying content.
 94 | 
 95 | ### Asset Management
 96 | 
 97 | - **list_assets**: List assets with pagination (3 items per page)
 98 | - **upload_asset**: Upload new assets with metadata
 99 | - **get_asset**: Retrieve asset details and information
100 | - **update_asset**: Update asset metadata and files
101 | - **delete_asset**: Remove assets from space
102 | - **publish_asset**: Publish assets to delivery API
103 | - **unpublish_asset**: Unpublish assets from delivery API
104 | 
105 | ### Space & Environment Management
106 | 
107 | - **list_spaces**: List available spaces
108 | - **get_space**: Get space details
109 | - **list_environments**: List environments in a space
110 | - **create_environment**: Create new environment
111 | - **delete_environment**: Remove environment
112 | 
113 | ### Content Type Management
114 | 
115 | - **list_content_types**: List available content types
116 | - **get_content_type**: Get content type details
117 | - **create_content_type**: Create new content type
118 | - **update_content_type**: Update content type
119 | - **delete_content_type**: Remove content type
120 | - **publish_content_type**: Publish a content type
121 | 
122 | ## Development Tools
123 | 
124 | ### MCP Inspector
125 | 
126 | The project includes an MCP Inspector tool that helps with development and debugging:
127 | 
128 | - **Inspect Mode**: Run `npm run inspect` to start the inspector, you can open the inspector by going to http://localhost:5173
129 | - **Watch Mode**: Use `npm run inspect:watch` to automatically restart the inspector when files change
130 | - **Visual Interface**: The inspector provides a web interface to test and debug MCP tools
131 | - **Real-time Testing**: Try out tools and see their responses immediately
132 | - **Bulk Operations Testing**: Test and monitor bulk operations with visual feedback on progress and results
133 | 
134 | The project also contains a `npm run dev` command which rebuilds and reloads the MCP server on every change.
135 | 
136 | ## Configuration
137 | 
138 | ### Prerequisites
139 | 
140 | 1. Create a Contentful account at [Contentful](https://www.contentful.com/)
141 | 2. Generate a Content Management API token from your account settings
142 | 
143 | ### Environment Variables
144 | 
145 | These variables can also be set as arguments
146 | 
147 | - `CONTENTFUL_HOST` / `--host`: Contentful Management API Endpoint (defaults to https://api.contentful.com)
148 | - `CONTENTFUL_MANAGEMENT_ACCESS_TOKEN` / `--management-token`: Your Content Management API token
149 | - `ENABLE_HTTP_SERVER` / `--http`: Set to "true" to enable HTTP/SSE mode
150 | - `HTTP_PORT` / `--port`: Port for HTTP server (default: 3000)
151 | - `HTTP_HOST` / `--http-host`: Host for HTTP server (default: localhost)
152 | 
153 | ### Space and Environment Scoping
154 | 
155 | You can scope the spaceId and EnvironmentId to ensure the LLM will only do operations on the defined space/env ID's.
156 | This is mainly to support agents that are to operate within specific spaces. If both `SPACE_ID` and `ENVIRONMENT_ID` env-vars are set
157 | the tools will not report needing these values and the handlers will use the environment vars to do CMA operations.
158 | You will also loose access to the tools in the space-handler, since these tools are across spaces.
159 | You can also add the `SPACE_ID` and `ENVIRONMENT_ID` by using arguments `--space-id` and `--environment-id`
160 | 
161 | #### Using App Identity
162 | 
163 | Instead of providing a Management token you can also leverage [App Identity](https://www.contentful.com/developers/docs/extensibility/app-framework/app-identity/)
164 | for handling authentication. You would have to setup and install a Contentful App and set the following parameters when calling the MCP-server:
165 | 
166 | - `--app-id` = the app Id which is providing the Apptoken
167 | - `--private-key` = the private key you created in the user-interface with your app, tied to `app_id`
168 | - `--space-id` = the spaceId in which the app is installed
169 | - `--environment-id` = the environmentId (within the space) in which the app is installed.
170 | 
171 | 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)
172 | 
173 | ### Usage with Claude Desktop
174 | 
175 | You do not need to clone this repo to use this MCP, you can simply add it to
176 | your `claude_desktop_config.json`:
177 | 
178 | Add or edit `~/Library/Application Support/Claude/claude_desktop_config.json`
179 | and add the following lines:
180 | 
181 | ```json
182 | {
183 |   "mcpServers": {
184 |     "contentful": {
185 |       "command": "npx",
186 |       "args": ["-y", "@ivotoby/contentful-management-mcp-server"],
187 |       "env": {
188 |         "CONTENTFUL_MANAGEMENT_ACCESS_TOKEN": "<Your CMA token>"
189 |       }
190 |     }
191 |   }
192 | }
193 | ```
194 | 
195 | If your MCPClient does not support setting environment variables you can also set the management token using an argument like this:
196 | 
197 | ```json
198 | {
199 |   "mcpServers": {
200 |     "contentful": {
201 |       "command": "npx",
202 |       "args": [
203 |         "-y",
204 |         "@ivotoby/contentful-management-mcp-server",
205 |         "--management-token",
206 |         "<your token>",
207 |         "--host",
208 |         "http://api.contentful.com"
209 |       ]
210 |     }
211 |   }
212 | }
213 | ```
214 | 
215 | ### Installing via Smithery
216 | 
217 | To install Contentful Management Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@ivotoby/contentful-management-mcp-server):
218 | 
219 | ```bash
220 | npx -y @smithery/cli install @ivotoby/contentful-management-mcp-server --client claude
221 | ```
222 | 
223 | ### Developing and using Claude desktop
224 | 
225 | If you want to contribute and test what Claude does with your contributions;
226 | 
227 | - run `npm run dev`, this will start the watcher that rebuilds the MCP server on every change
228 | - update `claude_desktop_config.json` to reference the project directly, ie;
229 | 
230 | ```
231 | {
232 |   "mcpServers": {
233 |     "contentful": {
234 |       "command": "node",
235 |       "args": ["/Users/ivo/workspace/contentful-mcp/bin/mcp-server.js"],
236 |       "env": {
237 |         "CONTENTFUL_MANAGEMENT_ACCESS_TOKEN": "<Your CMA Token>"
238 |       }
239 |     }
240 |   }
241 | }
242 | ```
243 | 
244 | 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
245 | 
246 | ## Transport Modes
247 | 
248 | The MCP server supports two transport modes:
249 | 
250 | ### stdio Transport
251 | 
252 | 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.
253 | 
254 | To use stdio mode, simply run the server without the `--http` flag:
255 | 
256 | ```bash
257 | npx -y contentful-mcp --management-token YOUR_TOKEN
258 | # or alternatively
259 | npx -y @ivotoby/contentful-management-mcp-server --management-token YOUR_TOKEN
260 | ```
261 | 
262 | ### StreamableHTTP Transport
263 | 
264 | 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.
265 | 
266 | To use StreamableHTTP mode, run with the `--http` flag:
267 | 
268 | ```bash
269 | npx -y contentful-mcp --management-token YOUR_TOKEN --http --port 3000
270 | # or alternatively
271 | npx -y @ivotoby/contentful-management-mcp-server --management-token YOUR_TOKEN --http --port 3000
272 | ```
273 | 
274 | #### StreamableHTTP Details
275 | 
276 | - Uses the official MCP StreamableHTTP transport
277 | - Supports standard MCP protocol operations
278 | - Includes session management for maintaining state
279 | - Properly handles initialize/notify patterns
280 | - Compatible with standard MCP clients
281 | - Replaces the deprecated SSE transport with the modern approach
282 | 
283 | The implementation follows the standard MCP protocol specification, allowing any MCP client to connect to the server without special handling.
284 | 
285 | ## Error Handling
286 | 
287 | The server implements comprehensive error handling for:
288 | 
289 | - Authentication failures
290 | - Rate limiting
291 | - Invalid requests
292 | - Network issues
293 | - API-specific errors
294 | 
295 | [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/146d4235-bdb1-492e-b594-82fd27b77388)
296 | 
297 | ## License
298 | 
299 | MIT License
300 | 
301 | ## Fine print
302 | 
303 | 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!
304 | 
305 | This MCP-server is not officially supported by Contentful (yet)
306 | 
```

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

```markdown
 1 | # Contentful MCP - Development Guide
 2 | 
 3 | ## Common Commands
 4 | - Build: `npm run build`
 5 | - Type Check: `npm run typecheck`
 6 | - Lint: `npm run lint`
 7 | - Run Tests: `npm test`
 8 | - Run Single Test: `npx vitest run test/path/to/test.test.ts`
 9 | - Run Tests in Watch Mode: `npm run test:watch`
10 | - Dev Mode (watch & rebuild): `npm run dev`
11 | 
12 | ## Code Style Guidelines
13 | - **Formatting**: Uses Prettier with 100 char width, no semicolons, double quotes
14 | - **TypeScript**: Use strict typing, avoid `any` when possible
15 | - **Imports**: Order from external to internal, group related imports
16 | - **Naming**: Use camelCase for variables/functions, PascalCase for types/interfaces
17 | - **Error Handling**: Always handle errors in async functions with try/catch blocks
18 | - **Documentation**: Add JSDoc style comments for functions and interfaces
19 | 
20 | ## Entity Structure
21 | - Tools and handlers are organized by entity type (Entry, Asset, Content Type, etc.)
22 | - Each handler should focus on a single responsibility
23 | - Bulk actions should use the Contentful API's bulk operation endpoints
24 | 
25 | ## Testing
26 | Tests use Vitest with MSW for API mocking. Organize tests in:
27 | - `test/unit/` - Unit tests for utility functions
28 | - `test/integration/` - Tests that verify handler behavior
```

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

```typescript
1 | import { handlePrompt } from "./handlePrompt";
2 | 
3 | // Re-export the handlePrompt function as the main export
4 | export { handlePrompt };
```

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

```javascript
 1 | // @ts-check
 2 | 
 3 | import eslint from "@eslint/js";
 4 | import tseslint from "typescript-eslint";
 5 | 
 6 | export default tseslint.config(
 7 |   eslint.configs.recommended,
 8 |   tseslint.configs.recommended,
 9 | );
10 | 
```

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

```typescript
 1 | import { defineConfig } from 'vitest/config'
 2 | 
 3 | export default defineConfig({
 4 |   test: {
 5 |     globals: true,
 6 |     environment: 'node',
 7 |     include: ['test/**/*.test.ts'],
 8 |     setupFiles: ['test/setup.ts']
 9 |   }
10 | })
11 | 
```

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

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

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

```javascript
 1 | import * as esbuild from "esbuild";
 2 | await esbuild.build({
 3 |   entryPoints: ["./src/index.ts"],
 4 |   bundle: true,
 5 |   platform: "node",
 6 |   format: "esm",
 7 |   outfile: "./dist/bundle.js",
 8 |   target: "node18",
 9 |   banner: {
10 |     js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);`,
11 |   },
12 | });
13 | 
```

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

```typescript
 1 | import { beforeAll, expect } from 'vitest';
 2 | import dotenv from "dotenv";
 3 | 
 4 | // Load environment variables from .env file
 5 | dotenv.config();
 6 | 
 7 | // Make sure we have the required environment variables
 8 | beforeAll(() => {
 9 |   const requiredEnvVars = ["CONTENTFUL_MANAGEMENT_ACCESS_TOKEN"];
10 | 
11 |   for (const envVar of requiredEnvVars) {
12 |     if (!process.env[envVar]) {
13 |       throw new Error(`Missing required environment variable: ${envVar}`);
14 |     }
15 |   }
16 | });
17 | 
18 | export { expect };
19 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "outDir": "./dist",
 4 |     "rootDir": ".",
 5 |     "target": "ES2022",
 6 |     "module": "ES2020",
 7 |     "moduleResolution": "node",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "sourceMap": false,
11 |     "skipLibCheck": true,
12 |     "forceConsistentCasingInFileNames": true,
13 |     "resolveJsonModule": true,
14 |     "allowImportingTsExtensions": true,
15 |     "noEmit": true
16 |   },
17 |   "include": [
18 |     "src/**/*",
19 |     "test/**/*",
20 |     "examples/**/*"
21 |   ],
22 |   "exclude": [
23 |     "node_modules",
24 |     "dist"
25 |   ]
26 | }
27 | 
```

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

```yaml
 1 | name: PR Check
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     types: [opened, synchronize, reopened]
 6 |     branches: 
 7 |       - main
 8 |       - master
 9 | 
10 | jobs:
11 |   verify:
12 |     name: Verify PR
13 |     runs-on: ubuntu-latest
14 |     steps:
15 |       - name: Checkout
16 |         uses: actions/checkout@v3
17 | 
18 |       - name: Setup Node.js
19 |         uses: actions/setup-node@v3
20 |         with:
21 |           node-version: "lts/*"
22 |           cache: 'npm'
23 | 
24 |       - name: Install dependencies
25 |         run: npm ci
26 | 
27 |       - name: Run tests
28 |         env:
29 |           CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN }}
30 |         run: npm test
31 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - contentfulManagementAccessToken
10 |     properties:
11 |       contentfulManagementAccessToken:
12 |         type: string
13 |         description: Your Content Management API token from Contentful
14 |   commandFunction:
15 |     # A function that produces the CLI command to start the MCP on stdio.
16 |     |-
17 |     (config) => ({ command: 'node', args: ['bin/mcp-server.js'], env: { CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: config.contentfulManagementAccessToken } })
```

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

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | import { fileURLToPath } from "url";
 4 | import { dirname, resolve } from "path";
 5 | 
 6 | const __filename = fileURLToPath(import.meta.url);
 7 | const __dirname = dirname(__filename);
 8 | 
 9 | const serverPath = resolve(__dirname, "../bin/mcp-server.js");
10 | 
11 | const args = ["npx", "@modelcontextprotocol/inspector", "node", serverPath];
12 | 
13 | // Add environment variables as CLI arguments if they exist
14 | if (process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN) {
15 |   args.push(`--headers=${process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN}`);
16 | }
17 | 
18 | // Execute the command
19 | import { spawn } from "child_process";
20 | const inspect = spawn(args[0], args.slice(1), { stdio: "inherit" });
21 | 
22 | inspect.on("error", (err) => {
23 |   console.error("Failed to start inspector:", err);
24 |   process.exit(1);
25 | });
26 | 
27 | inspect.on("exit", (code) => {
28 |   process.exit(code || 0);
29 | });
30 | 
```

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

```yaml
 1 | name: Release
 2 | on:
 3 |   push:
 4 |     branches:
 5 |       - main
 6 |       - master
 7 | 
 8 | permissions:
 9 |   contents: write
10 |   issues: write
11 |   pull-requests: write
12 | 
13 | jobs:
14 |   release:
15 |     name: Release
16 |     runs-on: ubuntu-latest
17 |     steps:
18 |       - name: Checkout
19 |         uses: actions/checkout@v3
20 |         with:
21 |           fetch-depth: 0
22 |           persist-credentials: false
23 | 
24 |       - name: Setup Node.js
25 |         uses: actions/setup-node@v3
26 |         with:
27 |           node-version: "lts/*"
28 |           cache: 'npm'
29 | 
30 |       - name: Install dependencies
31 |         run: npm ci
32 | 
33 |       - name: Run tests
34 |         env:
35 |           CONTENTFUL_MANAGEMENT_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN }}
36 |         run: npm test
37 | 
38 |       - name: Build
39 |         run: npm run build
40 | 
41 |       - name: Release
42 |         env:
43 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
45 |         run: npx semantic-release
46 | 
```

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

```typescript
 1 | import { GetPromptResult } from "@modelcontextprotocol/sdk/types";
 2 | import { contentfulHandlers } from "./promptHandlers/contentful";
 3 | import { aiActionsHandlers } from "./promptHandlers/aiActions";
 4 | 
 5 | /**
 6 |  * Handle a prompt request and return the appropriate response
 7 |  * @param name Prompt name
 8 |  * @param args Optional arguments provided for the prompt
 9 |  * @returns Prompt result with messages
10 |  */
11 | export async function handlePrompt(
12 |   name: string,
13 |   args?: Record<string, string>,
14 | ): Promise<GetPromptResult> {
15 |   // Check for AI Actions handlers
16 |   if (name.startsWith("ai-actions-") && name in aiActionsHandlers) {
17 |     return aiActionsHandlers[name as keyof typeof aiActionsHandlers](args);
18 |   }
19 |   
20 |   // Check for general Contentful handlers
21 |   if (name in contentfulHandlers) {
22 |     return contentfulHandlers[name as keyof typeof contentfulHandlers](args);
23 |   }
24 |   
25 |   // Handle unknown prompts
26 |   throw new Error(`Unknown prompt: ${name}`);
27 | }
```

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

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | # Use an official Node.js image as the base image
 3 | FROM node:22-alpine AS builder
 4 | 
 5 | # Set the working directory
 6 | WORKDIR /app
 7 | 
 8 | # Copy package files and source code
 9 | COPY . .
10 | 
11 | # Install dependencies
12 | RUN --mount=type=cache,target=/root/.npm npm install
13 | 
14 | # Build the application
15 | RUN npm run build
16 | 
17 | # Use a smaller Node.js image for the runtime
18 | FROM node:22-alpine AS runtime
19 | 
20 | # Set the working directory
21 | WORKDIR /app
22 | 
23 | # Copy built files from the builder stage
24 | COPY --from=builder /app/dist /app/dist
25 | COPY --from=builder /app/bin /app/bin
26 | COPY --from=builder /app/node_modules /app/node_modules
27 | COPY --from=builder /app/package.json /app/package.json
28 | 
29 | # Environment variable for Contentful Management API token
30 | ENV CONTENTFUL_MANAGEMENT_ACCESS_TOKEN=your_contentful_management_api_token
31 | 
32 | # Expose any required ports (if needed by the application)
33 | # EXPOSE 3000
34 | 
35 | # Start the server
36 | ENTRYPOINT ["node", "bin/mcp-server.js"]
```

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

```typescript
 1 | export function validateEnvironment(): void {
 2 |   const {
 3 |     CONTENTFUL_MANAGEMENT_ACCESS_TOKEN,
 4 |     PRIVATE_KEY,
 5 |     APP_ID,
 6 |     SPACE_ID,
 7 |     ENVIRONMENT_ID,
 8 |     ENABLE_HTTP_SERVER,
 9 |     HTTP_PORT
10 |   } = process.env
11 | 
12 |   if (!CONTENTFUL_MANAGEMENT_ACCESS_TOKEN && !PRIVATE_KEY) {
13 |     console.error("Either CONTENTFUL_MANAGEMENT_ACCESS_TOKEN or PRIVATE_KEY must be set")
14 |     process.exit(1)
15 |   }
16 | 
17 |   if (PRIVATE_KEY) {
18 |     if (!APP_ID) {
19 |       console.error("APP_ID is required when using PRIVATE_KEY")
20 |       process.exit(1)
21 |     }
22 |     if (!SPACE_ID) {
23 |       console.error("SPACE_ID is required when using PRIVATE_KEY")
24 |       process.exit(1)
25 |     }
26 |     if (!ENVIRONMENT_ID) {
27 |       console.error("ENVIRONMENT_ID is required when using PRIVATE_KEY")
28 |       process.exit(1)
29 |     }
30 |   }
31 | 
32 |   // Validate HTTP server settings if enabled
33 |   if (ENABLE_HTTP_SERVER === "true") {
34 |     if (HTTP_PORT) {
35 |       const port = parseInt(HTTP_PORT)
36 |       if (isNaN(port) || port < 1 || port > 65535) {
37 |         console.error("HTTP_PORT must be a valid port number (1-65535)")
38 |         process.exit(1)
39 |       }
40 |     }
41 |   }
42 | }
43 | 
```

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

```typescript
 1 | /* eslint-disable @typescript-eslint/no-explicit-any */
 2 | 
 3 | export interface SummarizeOptions {
 4 |   maxItems?: number
 5 |   indent?: number
 6 |   showTotal?: boolean
 7 |   remainingMessage?: string
 8 | }
 9 | 
10 | export const summarizeData = (data: any, options: SummarizeOptions = {}): any => {
11 |   const { maxItems = 3, remainingMessage = "To see more items, please ask me to retrieve the next page." } =
12 |     options
13 | 
14 |   // Handle Contentful-style responses with items and total
15 |   if (data && typeof data === "object" && "items" in data && "total" in data) {
16 |     const items = data.items
17 |     const total = data.total
18 | 
19 |     if (items.length <= maxItems) {
20 |       return data
21 |     }
22 | 
23 |     return {
24 |       items: items.slice(0, maxItems),
25 |       total: total,
26 |       showing: maxItems,
27 |       remaining: total - maxItems,
28 |       message: remainingMessage,
29 |       skip: maxItems // Add skip value for next page
30 |     }
31 |   }
32 | 
33 |   // Handle plain arrays
34 |   if (Array.isArray(data)) {
35 |     if (data.length <= maxItems) {
36 |       return data
37 |     }
38 | 
39 |     return {
40 |       items: data.slice(0, maxItems),
41 |       total: data.length,
42 |       showing: maxItems,
43 |       remaining: data.length - maxItems,
44 |       message: remainingMessage,
45 |       skip: maxItems // Add skip value for next page
46 |     }
47 |   }
48 | 
49 |   // Return non-array data as-is
50 |   return data
51 | }
52 | 
```

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

```typescript
 1 | import { getManagementToken } from "@contentful/node-apps-toolkit"
 2 | import { createClient } from "contentful-management"
 3 | 
 4 | const {
 5 |   CONTENTFUL_MANAGEMENT_ACCESS_TOKEN,
 6 |   CONTENTFUL_HOST = "api.contentful.com",
 7 |   PRIVATE_KEY,
 8 |   APP_ID,
 9 |   SPACE_ID,
10 |   ENVIRONMENT_ID,
11 | } = process.env
12 | 
13 | export const getContentfulClient = async () => {
14 |   let formattedKey = ""
15 |   if (!CONTENTFUL_MANAGEMENT_ACCESS_TOKEN && !PRIVATE_KEY) {
16 |     throw new Error("No Contentful management token or private key found...")
17 |   }
18 |   if (PRIVATE_KEY) {
19 |     const formatKey = (key: string) => {
20 |       // Remove existing headers, spaces, and line breaks
21 |       const cleanKey = key
22 |         .replace("-----BEGIN RSA PRIVATE KEY-----", "")
23 |         .replace("-----END RSA PRIVATE KEY-----", "")
24 |         .replace(/\s/g, "")
25 | 
26 |       // Split into 64-character lines
27 |       const chunks = cleanKey.match(/.{1,64}/g) || []
28 | 
29 |       // Reassemble with proper format
30 |       return ["-----BEGIN RSA PRIVATE KEY-----", ...chunks, "-----END RSA PRIVATE KEY-----"].join(
31 |         "\n",
32 |       )
33 |     }
34 | 
35 |     formattedKey = formatKey(PRIVATE_KEY!)
36 |   }
37 | 
38 |   const accessToken =
39 |     CONTENTFUL_MANAGEMENT_ACCESS_TOKEN ||
40 |     (await getManagementToken(formattedKey!, {
41 |       appInstallationId: APP_ID!,
42 |       spaceId: SPACE_ID!,
43 |       environmentId: ENVIRONMENT_ID!,
44 |       host: "https://" + CONTENTFUL_HOST,
45 |     }))
46 | 
47 |   return createClient(
48 |     {
49 |       accessToken,
50 |       host: CONTENTFUL_HOST,
51 |       headers: {
52 |         "X-Contentful-user-agent": "contentful-community-mcp/1.0.0",
53 |       },
54 |     },
55 |     { type: "plain" },
56 |   )
57 | }
58 | 
```

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

```typescript
 1 | import { describe, it, expect } from "vitest"
 2 | import fs from 'fs'
 3 | import path from 'path'
 4 | 
 5 | // We'll test the implementation by directly examining the source code
 6 | // since mocking the Contentful client has been causing issues
 7 | describe("AI Actions Alpha Header", () => {
 8 |   it("should have alpha header implementation in ai-actions-client.ts", () => {
 9 |     // Read the ai-actions-client.ts file
10 |     const clientPath = path.join(__dirname, '../../src/config/ai-actions-client.ts')
11 |     const fileContent = fs.readFileSync(clientPath, 'utf8')
12 |     
13 |     // Check for alpha header constants
14 |     expect(fileContent).toContain('X-Contentful-Enable-Alpha-Feature')
15 |     expect(fileContent).toContain('ai-service')
16 |     
17 |     // Check for withAlphaHeader function
18 |     expect(fileContent).toContain('function withAlphaHeader')
19 |     
20 |     // Check that the function is used in all API calls
21 |     const apiCalls = [
22 |       'client\\.raw\\.get\\(',
23 |       'client\\.raw\\.post\\(',
24 |       'client\\.raw\\.put\\(',
25 |       'client\\.raw\\.delete\\('
26 |     ]
27 |     
28 |     // Each API call should be followed by withAlphaHeader or include it as a parameter
29 |     apiCalls.forEach(call => {
30 |       // Count occurrences of the API call
31 |       const callMatches = fileContent.match(new RegExp(call, 'g')) || []
32 |       
33 |       // Count occurrences of withAlphaHeader near API calls
34 |       const withHeaderPattern = new RegExp(`${call}[^]*?withAlphaHeader`, 'g')
35 |       const withHeaderMatches = fileContent.match(withHeaderPattern) || []
36 |       
37 |       // Every API call should have a corresponding withAlphaHeader
38 |       expect(withHeaderMatches.length).toBeGreaterThan(0)
39 |       
40 |       // This isn't a strict test but checks that we're using the pattern broadly
41 |       console.log(`${call} occurrences: ${callMatches.length}, with header: ${withHeaderMatches.length}`)
42 |     })
43 |   })
44 | })
```

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

```typescript
 1 | import { getContentfulClient } from "../config/client.js"
 2 | 
 3 | export const spaceHandlers = {
 4 |   listSpaces: async () => {
 5 |     const contentfulClient = await getContentfulClient()
 6 |     const spaces = await contentfulClient.space.getMany({})
 7 |     return {
 8 |       content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }],
 9 |     }
10 |   },
11 | 
12 |   getSpace: async (args: { spaceId: string }) => {
13 |     const spaceId = args.spaceId
14 |     if (!spaceId) {
15 |       throw new Error("spaceId is required.")
16 |     }
17 | 
18 |     const contentfulClient = await getContentfulClient()
19 |     const space = await contentfulClient.space.get({ spaceId })
20 |     return {
21 |       content: [{ type: "text", text: JSON.stringify(space, null, 2) }],
22 |     }
23 |   },
24 | 
25 |   listEnvironments: async (args: { spaceId: string }) => {
26 |     const contentfulClient = await getContentfulClient()
27 |     const environments = await contentfulClient.environment.getMany({
28 |       spaceId: args.spaceId,
29 |     })
30 |     return {
31 |       content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
32 |     }
33 |   },
34 | 
35 |   createEnvironment: async (args: { spaceId: string; environmentId: string; name: string }) => {
36 |     const contentfulClient = await getContentfulClient()
37 |     const environment = await contentfulClient.environment.create(
38 |       {
39 |         spaceId: args.spaceId,
40 |         environmentId: args.environmentId,
41 |       },
42 |       {
43 |         name: args.name,
44 |       },
45 |     )
46 |     return {
47 |       content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
48 |     }
49 |   },
50 | 
51 |   deleteEnvironment: async (args: { spaceId: string; environmentId: string }) => {
52 |     const contentfulClient = await getContentfulClient()
53 |     await contentfulClient.environment.delete({
54 |       spaceId: args.spaceId,
55 |       environmentId: args.environmentId,
56 |     })
57 |     return {
58 |       content: [
59 |         {
60 |           type: "text",
61 |           text: `Environment ${args.environmentId} deleted successfully`,
62 |         },
63 |       ],
64 |     }
65 |   },
66 | }
67 | 
```

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

```javascript
 1 | #!/usr/bin/env node
 2 | /* eslint-disable no-undef */
 3 | 
 4 | async function main() {
 5 |   // Find the management token argument
 6 |   const tokenIndex = process.argv.findIndex((arg) => arg === "--management-token")
 7 |   if (tokenIndex !== -1 && process.argv[tokenIndex + 1]) {
 8 |     process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = process.argv[tokenIndex + 1]
 9 |   }
10 | 
11 |   const hostIndex = process.argv.findIndex((arg) => arg === "--host")
12 |   if (hostIndex !== -1 && process.argv[hostIndex + 1]) {
13 |     process.env.CONTENTFUL_HOST = process.argv[hostIndex + 1]
14 |   }
15 | 
16 |   const envIdIndex = process.argv.findIndex((arg) => arg === "--environment-id")
17 |   if (envIdIndex !== -1 && process.argv[envIdIndex + 1]) {
18 |     process.env.ENVIRONMENT_ID = process.argv[envIdIndex + 1]
19 |   }
20 | 
21 |   const spaceIdIndex = process.argv.findIndex((arg) => arg === "--space-id")
22 |   if (spaceIdIndex !== -1 && process.argv[spaceIdIndex + 1]) {
23 |     process.env.SPACE_ID = process.argv[spaceIdIndex + 1]
24 |   }
25 | 
26 |   const keyIdIndex = process.argv.findIndex((arg) => arg === "--private-key")
27 |   if (keyIdIndex !== -1 && process.argv[keyIdIndex + 1]) {
28 |     process.env.PRIVATE_KEY = process.argv[keyIdIndex + 1]
29 |   }
30 | 
31 |   const appIdIndex = process.argv.findIndex((arg) => arg === "--app-id")
32 |   if (appIdIndex !== -1 && process.argv[appIdIndex + 1]) {
33 |     process.env.APP_ID = process.argv[appIdIndex + 1]
34 |   }
35 | 
36 |   // Check for HTTP server mode flag
37 |   const httpServerFlagIndex = process.argv.findIndex((arg) => arg === "--http")
38 |   if (httpServerFlagIndex !== -1) {
39 |     process.env.ENABLE_HTTP_SERVER = "true"
40 | 
41 |     // Check for HTTP port
42 |     const httpPortIndex = process.argv.findIndex((arg) => arg === "--port")
43 |     if (httpPortIndex !== -1 && process.argv[httpPortIndex + 1]) {
44 |       process.env.HTTP_PORT = process.argv[httpPortIndex + 1]
45 |     }
46 | 
47 |     // Check for HTTP host
48 |     const httpHostIndex = process.argv.findIndex((arg) => arg === "--http-host")
49 |     if (httpHostIndex !== -1 && process.argv[httpHostIndex + 1]) {
50 |       process.env.HTTP_HOST = process.argv[httpHostIndex + 1]
51 |     }
52 |   }
53 | 
54 |   // Import and run the bundled server after env var is set
55 |   await import("../dist/bundle.js")
56 | }
57 | 
58 | main().catch((error) => {
59 |   console.error("Failed to start server:", error)
60 |   process.exit(1)
61 | })
62 | 
```

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

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { spawn } from 'child_process';
  4 | import nodemon from 'nodemon';
  5 | import { exec } from 'child_process';
  6 | 
  7 | let currentInspector = null;
  8 | let isShuttingDown = false;
  9 | 
 10 | // Function to kill all node processes running the inspector
 11 | function killAllInspectors() {
 12 |   return new Promise((resolve) => {
 13 |     if (process.platform === 'win32') {
 14 |       exec('taskkill /F /IM node.exe /FI "WINDOWTITLE eq @modelcontextprotocol/inspector*"');
 15 |     } else {
 16 |       exec('pkill -f "@modelcontextprotocol/inspector"');
 17 |     }
 18 |     resolve();
 19 |   });
 20 | }
 21 | 
 22 | // Function to run the inspector
 23 | function startInspector() {
 24 |   if (isShuttingDown) return null;
 25 |   
 26 |   const inspector = spawn('npm', ['run', 'inspect'], {
 27 |     stdio: 'inherit',
 28 |     shell: true
 29 |   });
 30 | 
 31 |   inspector.on('error', (err) => {
 32 |     console.error('Inspector failed to start:', err);
 33 |   });
 34 | 
 35 |   return inspector;
 36 | }
 37 | 
 38 | // Cleanup function
 39 | async function cleanup() {
 40 |   isShuttingDown = true;
 41 |   
 42 |   if (currentInspector) {
 43 |     currentInspector.kill('SIGTERM');
 44 |     currentInspector = null;
 45 |   }
 46 |   
 47 |   await killAllInspectors();
 48 |   nodemon.emit('quit');
 49 | }
 50 | 
 51 | // Set up nodemon to watch the src directory
 52 | nodemon({
 53 |   watch: ['src'],
 54 |   ext: 'ts',
 55 |   exec: 'npm run build'
 56 | });
 57 | 
 58 | // Handle nodemon events
 59 | nodemon
 60 |   .on('start', () => {
 61 |     console.log('Starting build...');
 62 |   })
 63 |   .on('restart', async () => {
 64 |     console.log('Files changed, rebuilding...');
 65 |     if (currentInspector) {
 66 |       currentInspector.kill('SIGTERM');
 67 |       await killAllInspectors();
 68 |     }
 69 |   })
 70 |   .on('quit', () => {
 71 |     console.log('Nodemon stopped');
 72 |     cleanup().then(() => process.exit(0));
 73 |   })
 74 |   .on('error', (err) => {
 75 |     console.error('Nodemon error:', err);
 76 |   })
 77 |   .on('crash', () => {
 78 |     console.error('Application crashed');
 79 |     cleanup();
 80 |   })
 81 |   .on('exit', () => {
 82 |     if (!isShuttingDown) {
 83 |       if (currentInspector) {
 84 |         currentInspector.kill('SIGTERM');
 85 |       }
 86 |       currentInspector = startInspector();
 87 |     }
 88 |   });
 89 | 
 90 | // Handle process termination
 91 | process.on('SIGTERM', cleanup);
 92 | process.on('SIGINT', cleanup);
 93 | process.on('SIGHUP', cleanup);
 94 | 
 95 | // Handle uncaught exceptions
 96 | process.on('uncaughtException', (err) => {
 97 |   console.error('Uncaught exception:', err);
 98 |   cleanup().then(() => process.exit(1));
 99 | });
100 | 
```

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

```json
 1 | {
 2 |   "name": "@ivotoby/contentful-management-mcp-server",
 3 |   "version": "1.14.0",
 4 |   "description": "MCP server for Contentful Content Management API integration",
 5 |   "license": "MIT",
 6 |   "type": "module",
 7 |   "main": "./dist/bundle.js",
 8 |   "repository": {
 9 |     "type": "git",
10 |     "url": "git+https://github.com/ivo-toby/contentful-mcp.git"
11 |   },
12 |   "bin": {
13 |     "mcp-server-contentful": "./bin/mcp-server.js",
14 |     "contentful-mcp": "./bin/mcp-server.js"
15 |   },
16 |   "files": [
17 |     "dist"
18 |   ],
19 |   "scripts": {
20 |     "build": "node build.js && chmod +x bin/mcp-server.js",
21 |     "clean": "rm -rf dist",
22 |     "lint": "eslint src/**/*.ts",
23 |     "watch": "tsc --watch",
24 |     "dev": "nodemon --watch src -e ts --exec 'npm run build'",
25 |     "typecheck": "tsc --noEmit",
26 |     "prepare": "npm run build",
27 |     "inspect": "node -r dotenv/config ./scripts/inspect.js",
28 |     "inspect-watch": "node ./scripts/inspect-watch.js",
29 |     "test": "vitest run --config vitest.config.ts",
30 |     "test:watch": "vitest watch --config vitest.config.ts"
31 |   },
32 |   "dependencies": {
33 |     "@contentful/node-apps-toolkit": "^3.13.0",
34 |     "@modelcontextprotocol/sdk": "1.11.1",
35 |     "contentful-management": "^11.52.2",
36 |     "cors": "^2.8.5",
37 |     "dotenv": "^16.5.0",
38 |     "express": "^4.18.3",
39 |     "zod": "^3.24.4",
40 |     "zod-to-json-schema": "^3.24.5"
41 |   },
42 |   "devDependencies": {
43 |     "@eslint/js": "^9.19.0",
44 |     "@semantic-release/commit-analyzer": "^11.1.0",
45 |     "@semantic-release/github": "^9.2.6",
46 |     "@semantic-release/npm": "^11.0.3",
47 |     "@semantic-release/release-notes-generator": "^12.1.0",
48 |     "@types/chai": "^4.3.11",
49 |     "@types/cors": "^2.8.17",
50 |     "@types/express": "^4.17.21",
51 |     "@types/mocha": "^10.0.6",
52 |     "@types/node": "^20.10.0",
53 |     "@types/sinon": "^17.0.3",
54 |     "@types/supertest": "^6.0.2",
55 |     "@typescript-eslint/eslint-plugin": "^6.12.0",
56 |     "@typescript-eslint/parser": "^6.12.0",
57 |     "chai": "^5.0.0",
58 |     "esbuild": "^0.19.9",
59 |     "eslint": "^8.57.1",
60 |     "eslint-plugin-perfectionist": "^4.7.0",
61 |     "mocha": "^10.2.0",
62 |     "msw": "^2.7.0",
63 |     "nodemon": "^3.1.9",
64 |     "prettier": "^3.4.2",
65 |     "semantic-release": "^22.0.12",
66 |     "sinon": "^17.0.1",
67 |     "supertest": "^6.3.3",
68 |     "ts-node": "^10.9.2",
69 |     "typescript": "^5.6.2",
70 |     "typescript-eslint": "^8.22.0",
71 |     "vitest": "^3.1.3"
72 |   }
73 | }
74 | 
```

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

```typescript
 1 | import { describe, it, expect, beforeEach } from "vitest"
 2 | import { getSpaceEnvProperties } from "../../src/types/tools"
 3 | 
 4 | describe("getSpaceEnvProperties", () => {
 5 |   const originalEnv = process.env
 6 | 
 7 |   beforeEach(() => {
 8 |     process.env = { ...originalEnv }
 9 |   })
10 | 
11 |   afterEach(() => {
12 |     process.env = originalEnv
13 |   })
14 | 
15 |   it("should add spaceId and environmentId properties when environment variables are not set", () => {
16 |     delete process.env.SPACE_ID
17 |     delete process.env.ENVIRONMENT_ID
18 | 
19 |     const config = {
20 |       type: "object",
21 |       properties: {
22 |         existingProperty: { type: "string" },
23 |       },
24 |       required: ["existingProperty"],
25 |     }
26 | 
27 |     const result = getSpaceEnvProperties(config)
28 | 
29 |     expect(result.properties).toHaveProperty("spaceId")
30 |     expect(result.properties).toHaveProperty("environmentId")
31 |     expect(result.required).toContain("spaceId")
32 |     expect(result.required).toContain("environmentId")
33 |   })
34 | 
35 |   it("should not add spaceId and environmentId properties when environment variables are set", () => {
36 |     process.env.SPACE_ID = "test-space-id"
37 |     process.env.ENVIRONMENT_ID = "test-environment-id"
38 | 
39 |     const config = {
40 |       type: "object",
41 |       properties: {
42 |         existingProperty: { type: "string" },
43 |       },
44 |       required: ["existingProperty"],
45 |     }
46 | 
47 |     const result = getSpaceEnvProperties(config)
48 | 
49 |     expect(result.properties).not.toHaveProperty("spaceId")
50 |     expect(result.properties).not.toHaveProperty("environmentId")
51 |     expect(result.required).not.toContain("spaceId")
52 |     expect(result.required).not.toContain("environmentId")
53 |   })
54 | 
55 |   it("should merge spaceId and environmentId properties with existing properties", () => {
56 |     delete process.env.SPACE_ID
57 |     delete process.env.ENVIRONMENT_ID
58 | 
59 |     const config = {
60 |       type: "object",
61 |       properties: {
62 |         existingProperty: { type: "string" },
63 |       },
64 |       required: ["existingProperty"],
65 |     }
66 | 
67 |     const result = getSpaceEnvProperties(config)
68 | 
69 |     expect(result.properties).toHaveProperty("existingProperty")
70 |     expect(result.properties).toHaveProperty("spaceId")
71 |     expect(result.properties).toHaveProperty("environmentId")
72 |     expect(result.required).toContain("existingProperty")
73 |     expect(result.required).toContain("spaceId")
74 |     expect(result.required).toContain("environmentId")
75 |   })
76 | })
77 | 
```

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

```typescript
 1 | import { describe, it, expect } from "vitest"
 2 | import { getAiActionTools } from "../../src/types/tools"
 3 | 
 4 | describe("AI Action Tool Definitions", () => {
 5 |   const tools = getAiActionTools()
 6 |   
 7 |   it("should export the correct AI Action tools", () => {
 8 |     expect(tools).toHaveProperty("LIST_AI_ACTIONS")
 9 |     expect(tools).toHaveProperty("GET_AI_ACTION")
10 |     expect(tools).toHaveProperty("CREATE_AI_ACTION")
11 |     expect(tools).toHaveProperty("UPDATE_AI_ACTION")
12 |     expect(tools).toHaveProperty("DELETE_AI_ACTION")
13 |     expect(tools).toHaveProperty("PUBLISH_AI_ACTION")
14 |     expect(tools).toHaveProperty("UNPUBLISH_AI_ACTION")
15 |     expect(tools).toHaveProperty("INVOKE_AI_ACTION")
16 |     expect(tools).toHaveProperty("GET_AI_ACTION_INVOCATION")
17 |   })
18 |   
19 |   it("should have the correct schema for LIST_AI_ACTIONS", () => {
20 |     const tool = tools.LIST_AI_ACTIONS
21 |     
22 |     expect(tool.name).toBe("list_ai_actions")
23 |     expect(tool.description).toContain("List all AI Actions")
24 |     
25 |     const schema = tool.inputSchema
26 |     expect(schema.properties).toHaveProperty("limit")
27 |     expect(schema.properties).toHaveProperty("skip")
28 |     expect(schema.properties).toHaveProperty("status")
29 |   })
30 |   
31 |   it("should have the correct schema for INVOKE_AI_ACTION", () => {
32 |     const tool = tools.INVOKE_AI_ACTION
33 |     
34 |     expect(tool.name).toBe("invoke_ai_action")
35 |     expect(tool.description).toContain("Invoke an AI Action")
36 |     
37 |     const schema = tool.inputSchema
38 |     expect(schema.properties).toHaveProperty("aiActionId")
39 |     expect(schema.properties).toHaveProperty("variables")
40 |     expect(schema.properties).toHaveProperty("rawVariables")
41 |     expect(schema.properties).toHaveProperty("outputFormat")
42 |     expect(schema.properties).toHaveProperty("waitForCompletion")
43 |     
44 |     // Variables should be an object with free-form properties
45 |     expect(schema.properties.variables.type).toBe("object")
46 |     expect(schema.properties.variables.additionalProperties).toBeDefined()
47 |     
48 |     // outputFormat should be an enum
49 |     expect(schema.properties.outputFormat.enum).toContain("Markdown")
50 |     expect(schema.properties.outputFormat.enum).toContain("RichText")
51 |     expect(schema.properties.outputFormat.enum).toContain("PlainText")
52 |     
53 |     // aiActionId should be required
54 |     expect(schema.required).toContain("aiActionId")
55 |   })
56 |   
57 |   it("should have the correct schema for CREATE_AI_ACTION", () => {
58 |     const tool = tools.CREATE_AI_ACTION
59 |     
60 |     expect(tool.name).toBe("create_ai_action")
61 |     expect(tool.description).toContain("Create a new AI Action")
62 |     
63 |     const schema = tool.inputSchema
64 |     expect(schema.properties).toHaveProperty("name")
65 |     expect(schema.properties).toHaveProperty("description")
66 |     expect(schema.properties).toHaveProperty("instruction")
67 |     expect(schema.properties).toHaveProperty("configuration")
68 |     
69 |     // Instruction should have template and variables
70 |     expect(schema.properties.instruction.properties).toHaveProperty("template")
71 |     expect(schema.properties.instruction.properties).toHaveProperty("variables")
72 |     
73 |     // Configuration should have modelType and modelTemperature
74 |     expect(schema.properties.configuration.properties).toHaveProperty("modelType")
75 |     expect(schema.properties.configuration.properties).toHaveProperty("modelTemperature")
76 |     
77 |     // Required fields
78 |     expect(schema.required).toContain("name")
79 |     expect(schema.required).toContain("description")
80 |     expect(schema.required).toContain("instruction")
81 |     expect(schema.required).toContain("configuration")
82 |   })
83 | })
```

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

```typescript
  1 | import { expect, vi } from "vitest"
  2 | import { server } from "../msw-setup.js"
  3 | 
  4 | // Define mock values first - these can be before vi.mock
  5 | const TEST_ENTRY_ID = "test-entry-id"
  6 | const TEST_ASSET_ID = "test-asset-id"
  7 | const TEST_BULK_ACTION_ID = "test-bulk-action-id"
  8 | 
  9 | // Mock the client module without using variables defined later
 10 | vi.mock("../../src/config/client.ts", () => {
 11 |   return {
 12 |     getContentfulClient: vi.fn().mockResolvedValue({
 13 |       entry: {
 14 |         get: vi.fn().mockResolvedValue({
 15 |           sys: { id: "test-entry-id", version: 1 },
 16 |         }),
 17 |       },
 18 |       asset: {
 19 |         get: vi.fn().mockResolvedValue({
 20 |           sys: { id: "test-asset-id", version: 1 },
 21 |         }),
 22 |       },
 23 |       bulkAction: {
 24 |         publish: vi.fn().mockResolvedValue({
 25 |           sys: { id: "test-bulk-action-id", status: "created" },
 26 |         }),
 27 |         unpublish: vi.fn().mockResolvedValue({
 28 |           sys: { id: "test-bulk-action-id", status: "created" },
 29 |         }),
 30 |         validate: vi.fn().mockResolvedValue({
 31 |           sys: { id: "test-bulk-action-id", status: "created" },
 32 |         }),
 33 |         get: vi.fn().mockResolvedValue({
 34 |           sys: { id: "test-bulk-action-id", status: "succeeded" },
 35 |           succeeded: [
 36 |             { sys: { id: "test-entry-id", type: "Entry" } },
 37 |             { sys: { id: "test-asset-id", type: "Asset" } },
 38 |           ],
 39 |         }),
 40 |       },
 41 |     }),
 42 |   }
 43 | })
 44 | 
 45 | // Import handlers after mocking
 46 | import { bulkActionHandlers } from "../../src/handlers/bulk-action-handlers.ts"
 47 | 
 48 | describe("Bulk Action Handlers Integration Tests", () => {
 49 |   // Start MSW Server before tests
 50 |   beforeAll(() => server.listen())
 51 |   afterEach(() => server.resetHandlers())
 52 |   afterAll(() => server.close())
 53 | 
 54 |   const testSpaceId = "test-space-id"
 55 |   const testEnvironmentId = "master"
 56 | 
 57 |   describe("bulkPublish", () => {
 58 |     it("should publish multiple entries and assets", async () => {
 59 |       const result = await bulkActionHandlers.bulkPublish({
 60 |         spaceId: testSpaceId,
 61 |         environmentId: testEnvironmentId,
 62 |         entities: [
 63 |           { sys: { id: TEST_ENTRY_ID, type: "Entry" } },
 64 |           { sys: { id: TEST_ASSET_ID, type: "Asset" } },
 65 |         ],
 66 |       })
 67 | 
 68 |       expect(result).to.have.property("content").that.is.an("array")
 69 |       expect(result.content[0].text).to.include("Bulk publish completed")
 70 |       expect(result.content[0].text).to.include("Successfully processed")
 71 |     })
 72 |   })
 73 | 
 74 |   describe("bulkUnpublish", () => {
 75 |     it("should unpublish multiple entries and assets", async () => {
 76 |       const result = await bulkActionHandlers.bulkUnpublish({
 77 |         spaceId: testSpaceId,
 78 |         environmentId: testEnvironmentId,
 79 |         entities: [
 80 |           { sys: { id: TEST_ENTRY_ID, type: "Entry" } },
 81 |           { sys: { id: TEST_ASSET_ID, type: "Asset" } },
 82 |         ],
 83 |       })
 84 | 
 85 |       expect(result).to.have.property("content").that.is.an("array")
 86 |       expect(result.content[0].text).to.include("Bulk unpublish completed")
 87 |       expect(result.content[0].text).to.include("Successfully processed")
 88 |     })
 89 |   })
 90 | 
 91 |   describe("bulkValidate", () => {
 92 |     it("should validate multiple entries", async () => {
 93 |       const result = await bulkActionHandlers.bulkValidate({
 94 |         spaceId: testSpaceId,
 95 |         environmentId: testEnvironmentId,
 96 |         entryIds: [TEST_ENTRY_ID],
 97 |       })
 98 | 
 99 |       expect(result).to.have.property("content").that.is.an("array")
100 |       expect(result.content[0].text).to.include("Bulk validation completed")
101 |       expect(result.content[0].text).to.include("Successfully validated")
102 |     })
103 |   })
104 | })
105 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
  2 | import { server } from "../msw-setup"
  3 | 
  4 | // Mock these modules at the top level
  5 | vi.mock("@contentful/node-apps-toolkit", () => ({
  6 |   getManagementToken: vi.fn(),
  7 | }))
  8 | vi.mock("contentful-management", () => ({
  9 |   createClient: vi.fn(),
 10 | }))
 11 | 
 12 | describe("getContentfulClient", () => {
 13 |   beforeEach(() => {
 14 |     server.listen()
 15 |   })
 16 | 
 17 |   afterEach(() => {
 18 |     server.resetHandlers()
 19 |     server.close()
 20 |     vi.resetModules()
 21 |     vi.clearAllMocks()
 22 |   })
 23 | 
 24 |   it("uses CONTENTFUL_MANAGEMENT_ACCESS_TOKEN if available", async () => {
 25 |     process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = "test-token"
 26 |     process.env.CONTENTFUL_HOST = "api.contentful.com"
 27 | 
 28 |     const mockCreateClient = vi.fn()
 29 |     const { createClient } = await import("contentful-management")
 30 |     vi.mocked(createClient).mockImplementation(mockCreateClient)
 31 | 
 32 |     const { getContentfulClient } = await import("../../src/config/client")
 33 |     await getContentfulClient()
 34 | 
 35 |     expect(mockCreateClient).toHaveBeenCalledWith(
 36 |       {
 37 |         accessToken: "test-token",
 38 |         host: "api.contentful.com",
 39 |         headers: {
 40 |           "X-Contentful-user-agent": "contentful-community-mcp/1.0.0",
 41 |         },
 42 |       },
 43 |       { type: "plain" },
 44 |     )
 45 |   })
 46 | 
 47 |   it("gets a token using PRIVATE_KEY and APP_ID if MANAGEMENT_ACCESS_TOKEN not available", async () => {
 48 |     delete process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN
 49 |     process.env.PRIVATE_KEY = "test-private-key"
 50 |     process.env.APP_ID = "test-app-id"
 51 |     process.env.SPACE_ID = "test-space-id"
 52 |     process.env.ENVIRONMENT_ID = "test-environment-id"
 53 |     process.env.CONTENTFUL_HOST = "api.contentful.com"
 54 | 
 55 |     const { getManagementToken } = await import("@contentful/node-apps-toolkit")
 56 |     const { createClient } = await import("contentful-management")
 57 | 
 58 |     vi.mocked(getManagementToken).mockResolvedValue("generated-token")
 59 |     const mockCreateClient = vi.fn()
 60 |     vi.mocked(createClient).mockImplementation(mockCreateClient)
 61 | 
 62 |     const { getContentfulClient } = await import("../../src/config/client")
 63 |     await getContentfulClient()
 64 | 
 65 |     expect(getManagementToken).toHaveBeenCalledWith(
 66 |       "-----BEGIN RSA PRIVATE KEY-----\ntest-private-key\n-----END RSA PRIVATE KEY-----",
 67 |       {
 68 |         appInstallationId: "test-app-id",
 69 |         spaceId: "test-space-id",
 70 |         environmentId: "test-environment-id",
 71 |         host: "https://api.contentful.com",
 72 |       },
 73 |     )
 74 | 
 75 |     expect(mockCreateClient).toHaveBeenCalledWith(
 76 |       {
 77 |         accessToken: "generated-token",
 78 |         host: "api.contentful.com",
 79 |         headers: {
 80 |           "X-Contentful-user-agent": "contentful-community-mcp/1.0.0",
 81 |         },
 82 |       },
 83 |       { type: "plain" },
 84 |     )
 85 |   })
 86 | 
 87 |   it("includes MCP identification header in all client configurations", async () => {
 88 |     process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = "test-token"
 89 |     process.env.CONTENTFUL_HOST = "api.contentful.com"
 90 | 
 91 |     const mockCreateClient = vi.fn()
 92 |     const { createClient } = await import("contentful-management")
 93 |     vi.mocked(createClient).mockImplementation(mockCreateClient)
 94 | 
 95 |     const { getContentfulClient } = await import("../../src/config/client")
 96 |     await getContentfulClient()
 97 | 
 98 |     const callArgs = mockCreateClient.mock.calls[0]
 99 |     const config = callArgs[0]
100 | 
101 |     expect(config.headers).toBeDefined()
102 |     expect(config.headers["X-Contentful-user-agent"]).toBe("contentful-community-mcp/1.0.0")
103 |   })
104 | 
105 |   it("throws if neither CONTENTFUL_MANAGEMENT_ACCESS_TOKEN nor PRIVATE_KEY is available", async () => {
106 |     delete process.env.CONTENTFUL_MANAGEMENT_ACCESS_TOKEN
107 |     delete process.env.PRIVATE_KEY
108 | 
109 |     vi.resetModules()
110 | 
111 |     const { getContentfulClient } = await import("../../src/config/client")
112 | 
113 |     await expect(() => getContentfulClient()).rejects.toThrow(
114 |       "No Contentful management token or private key found...",
115 |     )
116 |   })
117 | })
118 | 
```

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

```typescript
  1 | /**
  2 |  * Handler for providing a comprehensive overview of AI Actions in Contentful
  3 |  */
  4 | export async function handleAiActionsOverview() {
  5 |   return {
  6 |     messages: [
  7 |       {
  8 |         role: "assistant",
  9 |         content: {
 10 |           type: "text",
 11 |           text: "I'm your Contentful AI Actions specialist. I can explain how AI Actions integrate AI capabilities directly into your content management workflows.",
 12 |         },
 13 |       },
 14 |       {
 15 |         role: "user",
 16 |         content: {
 17 |           type: "text",
 18 |           text: `Please explain what AI Actions are in Contentful, how they work, and how they relate to content entries and fields.`,
 19 |         },
 20 |       },
 21 |       {
 22 |         role: "assistant",
 23 |         content: {
 24 |           type: "text",
 25 |           text: `# AI Actions in Contentful
 26 | 
 27 | 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.
 28 | 
 29 | ## Key Concepts
 30 | 
 31 | 1. **Templates with Variables**: Each AI Action contains a prompt template with placeholders for variables that users can provide when invoking the action.
 32 | 
 33 | 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.
 34 | 
 35 | 3. **Asynchronous Processing**: When invoked, AI Actions run asynchronously in the background, allowing content editors to continue working while waiting for results.
 36 | 
 37 | 4. **Flexible Variable Types**: Support for various input types:
 38 |    - Simple text inputs
 39 |    - References to other entries
 40 |    - Media references
 41 |    - Locale selection
 42 |    - Predefined option lists
 43 | 
 44 | 5. **Field-Level Operations**: AI Actions can be applied to specific fields within entries using the entity path parameter.
 45 | 
 46 | ## How AI Actions Work
 47 | 
 48 | 1. **Creation**: Developers or content managers define AI Actions with:
 49 |    - A name and description
 50 |    - A prompt template
 51 |    - Variable definitions
 52 |    - AI model configuration (model type, temperature)
 53 | 
 54 | 2. **Publication**: Actions are published to make them available to content editors.
 55 | 
 56 | 3. **Invocation**: Content editors can:
 57 |    - Select an AI Action from the UI
 58 |    - Fill in required variables
 59 |    - Apply it to specific content
 60 |    - Receive AI-generated content they can review and incorporate
 61 | 
 62 | 4. **Results**: The AI-generated content is presented to the editor who can then:
 63 |    - Accept it as is
 64 |    - Edit it further
 65 |    - Reject it and try again with different parameters
 66 | 
 67 | ## Practical Applications
 68 | 
 69 | - Generating SEO metadata from existing content
 70 | - Creating alt text for images
 71 | - Translating content between languages
 72 | - Summarizing long-form content
 73 | - Enhancing product descriptions
 74 | - Creating variations of existing content
 75 | - Improving grammar and readability
 76 | 
 77 | ## Using AI Actions via MCP
 78 | 
 79 | When using this MCP integration, you can:
 80 | 1. Create and manage AI Actions using the management tools
 81 | 2. Invoke existing AI Actions on specific content
 82 | 3. Process the results for further use
 83 | 
 84 | Each published AI Action becomes available as a dynamic tool with its own parameters based on its variable definitions.
 85 | 
 86 | ## Working with Complex Variables
 87 | 
 88 | One of the most important aspects to understand is how to work with References and MediaReferences:
 89 | 
 90 | - They require both an ID parameter (which entry/asset to use)
 91 | - And a path parameter (which field within that entry/asset to access)
 92 | 
 93 | This two-part approach gives you precise control over what content is processed by the AI Action.
 94 | 
 95 | ## Understanding the Output
 96 | 
 97 | 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.
 98 | 
 99 | Would you like me to explain any specific aspect of AI Actions in more detail?`,
100 |         },
101 |       },
102 |     ],
103 |   };
104 | }
```

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

```typescript
  1 | import { expect } from "vitest";
  2 | import { assetHandlers } from "../../src/handlers/asset-handlers.js";
  3 | import { server } from "../msw-setup.js";
  4 | 
  5 | describe("Asset Handlers Integration Tests", () => {
  6 |   // Start MSW Server before tests
  7 |   beforeAll(() => server.listen());
  8 |   afterEach(() => server.resetHandlers());
  9 |   afterAll(() => server.close());
 10 | 
 11 |   const testSpaceId = "test-space-id";
 12 |   const testAssetId = "test-asset-id";
 13 | 
 14 |   describe("uploadAsset", () => {
 15 |     it("should upload and process a new asset", async () => {
 16 |       const uploadData = {
 17 |         spaceId: testSpaceId,
 18 |         title: "Test Asset",
 19 |         description: "Test Description",
 20 |         file: {
 21 |           fileName: "test.jpg",
 22 |           contentType: "image/jpeg",
 23 |           upload: "https://example.com/test.jpg",
 24 |         },
 25 |       };
 26 | 
 27 |       const result = await assetHandlers.uploadAsset(uploadData);
 28 | 
 29 |       // Verify the response structure
 30 |       expect(result).to.have.property("content").that.is.an("array");
 31 |       expect(result.content).to.have.lengthOf(1);
 32 |       expect(result.content[0]).to.have.property("type", "text");
 33 | 
 34 |       // Parse and verify the asset data
 35 |       const asset = JSON.parse(result.content[0].text);
 36 |       expect(asset).to.have.nested.property("sys.id", "test-asset-id");
 37 |       expect(asset).to.have.nested.property("sys.version").that.is.a("number");
 38 |       expect(asset).to.have.nested.property("fields.title.en-US", "Test Asset");
 39 |       expect(asset).to.have.nested.property(
 40 |         "fields.description.en-US",
 41 |         "Test Description",
 42 |       );
 43 |       expect(asset).to.have.nested.property("fields.file.en-US").that.includes({
 44 |         fileName: "test.jpg",
 45 |         contentType: "image/jpeg",
 46 |       });
 47 |     });
 48 |   });
 49 |   describe("getAsset", () => {
 50 |     it("should get details of a specific asset", async () => {
 51 |       const result = await assetHandlers.getAsset({
 52 |         spaceId: testSpaceId,
 53 |         assetId: testAssetId,
 54 |       });
 55 | 
 56 |       expect(result).to.have.property("content");
 57 |       const asset = JSON.parse(result.content[0].text);
 58 |       expect(asset.sys.id).to.equal(testAssetId);
 59 |     });
 60 | 
 61 |     it("should throw error for invalid asset ID", async () => {
 62 |       try {
 63 |         await assetHandlers.getAsset({
 64 |           spaceId: testSpaceId,
 65 |           assetId: "invalid-asset-id",
 66 |         });
 67 |         expect.fail("Should have thrown an error");
 68 |       } catch (error) {
 69 |         expect(error).to.exist;
 70 |       }
 71 |     });
 72 |   });
 73 | 
 74 |   describe("updateAsset", () => {
 75 |     it("should update an existing asset", async () => {
 76 |       const result = await assetHandlers.updateAsset({
 77 |         spaceId: testSpaceId,
 78 |         assetId: testAssetId,
 79 |         title: "Updated Asset",
 80 |         description: "Updated Description",
 81 |       });
 82 | 
 83 |       expect(result).to.have.property("content");
 84 |       const asset = JSON.parse(result.content[0].text);
 85 |       expect(asset.fields.title["en-US"]).to.equal("Updated Asset");
 86 |       expect(asset.fields.description["en-US"]).to.equal("Updated Description");
 87 |     });
 88 |   });
 89 | 
 90 |   describe("deleteAsset", () => {
 91 |     it("should delete an asset", async () => {
 92 |       const result = await assetHandlers.deleteAsset({
 93 |         spaceId: testSpaceId,
 94 |         assetId: testAssetId,
 95 |       });
 96 | 
 97 |       expect(result).to.have.property("content");
 98 |       expect(result.content[0].text).to.include("deleted successfully");
 99 |     });
100 |   });
101 | 
102 |   describe("publishAsset", () => {
103 |     it("should publish an asset", async () => {
104 |       const result = await assetHandlers.publishAsset({
105 |         spaceId: testSpaceId,
106 |         assetId: testAssetId,
107 |       });
108 | 
109 |       expect(result).to.have.property("content");
110 |       const asset = JSON.parse(result.content[0].text);
111 |       expect(asset.sys.publishedVersion).to.exist;
112 |     });
113 |   });
114 | 
115 |   describe("unpublishAsset", () => {
116 |     it("should unpublish an asset", async () => {
117 |       const result = await assetHandlers.unpublishAsset({
118 |         spaceId: testSpaceId,
119 |         assetId: testAssetId,
120 |       });
121 | 
122 |       expect(result).to.have.property("content");
123 |       const asset = JSON.parse(result.content[0].text);
124 |       expect(asset.sys.publishedVersion).to.not.exist;
125 |     });
126 |   });
127 | });
128 | 
```

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

```typescript
  1 | import { describe, it, expect } from "vitest"
  2 | import type {
  3 |   AiActionEntity,
  4 |   Variable,
  5 |   Instruction,
  6 |   Configuration,
  7 |   VariableValue,
  8 |   AiActionInvocation
  9 | } from "../../src/types/ai-actions"
 10 | 
 11 | describe("AI Action Types", () => {
 12 |   it("should validate Variable type structure", () => {
 13 |     // Test creating variables of different types
 14 |     const textVariable: Variable = {
 15 |       id: "text-var",
 16 |       type: "Text",
 17 |       name: "Text Variable",
 18 |       description: "A text variable"
 19 |     }
 20 | 
 21 |     const optionsVariable: Variable = {
 22 |       id: "options-var",
 23 |       type: "StringOptionsList",
 24 |       name: "Options Variable",
 25 |       configuration: {
 26 |         values: ["option1", "option2", "option3"],
 27 |         allowFreeFormInput: false
 28 |       }
 29 |     }
 30 | 
 31 |     const referenceVariable: Variable = {
 32 |       id: "ref-var",
 33 |       type: "Reference",
 34 |       name: "Reference Variable",
 35 |       configuration: {
 36 |         allowedEntities: ["Entry"]
 37 |       } as any
 38 |     }
 39 | 
 40 |     expect(textVariable.id).toBe("text-var")
 41 |     expect(optionsVariable.type).toBe("StringOptionsList")
 42 |     expect((referenceVariable.configuration as any)?.allowedEntities).toContain("Entry")
 43 |   })
 44 | 
 45 |   it("should validate Instruction type structure", () => {
 46 |     const instruction: Instruction = {
 47 |       template: "This is a template with {{var1}} and {{var2}}",
 48 |       variables: [
 49 |         { id: "var1", type: "Text" },
 50 |         { id: "var2", type: "StandardInput" }
 51 |       ],
 52 |       conditions: [
 53 |         { id: "cond1", variable: "var1", operator: "eq", value: "some value" }
 54 |       ]
 55 |     }
 56 | 
 57 |     expect(instruction.template).toContain("{{var1}}")
 58 |     expect(instruction.variables).toHaveLength(2)
 59 |     expect(instruction.conditions?.[0].operator).toBe("eq")
 60 |   })
 61 | 
 62 |   it("should validate Configuration type structure", () => {
 63 |     const config: Configuration = {
 64 |       modelType: "gpt-4",
 65 |       modelTemperature: 0.7
 66 |     }
 67 | 
 68 |     expect(config.modelType).toBe("gpt-4")
 69 |     expect(config.modelTemperature).toBe(0.7)
 70 |   })
 71 | 
 72 |   it("should validate variable value structure", () => {
 73 |     const textValue: VariableValue = {
 74 |       id: "text-var",
 75 |       value: "some text value"
 76 |     }
 77 | 
 78 |     const refValue: VariableValue = {
 79 |       id: "ref-var",
 80 |       value: {
 81 |         entityType: "Entry",
 82 |         entityId: "entry123",
 83 |         entityPath: "fields.title"
 84 |       }
 85 |     }
 86 | 
 87 |     expect(textValue.value).toBe("some text value")
 88 |     expect(refValue.value.entityType).toBe("Entry")
 89 |   })
 90 | 
 91 |   it("should validate AiActionEntity structure", () => {
 92 |     const entity: AiActionEntity = {
 93 |       sys: {
 94 |         id: "action1",
 95 |         type: "AiAction",
 96 |         createdAt: "2023-01-01T00:00:00Z",
 97 |         updatedAt: "2023-01-02T00:00:00Z",
 98 |         version: 1,
 99 |         space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
100 |         createdBy: { sys: { id: "user1", linkType: "User", type: "Link" } },
101 |         updatedBy: { sys: { id: "user1", linkType: "User", type: "Link" } }
102 |       },
103 |       name: "Test Action",
104 |       description: "A test action",
105 |       instruction: {
106 |         template: "Template with {{var}}",
107 |         variables: [{ id: "var", type: "Text" }]
108 |       },
109 |       configuration: {
110 |         modelType: "gpt-4",
111 |         modelTemperature: 0.5
112 |       }
113 |     }
114 | 
115 |     expect(entity.sys.id).toBe("action1")
116 |     expect(entity.name).toBe("Test Action")
117 |     expect(entity.instruction.variables).toHaveLength(1)
118 |   })
119 | 
120 |   it("should validate AiActionInvocation structure", () => {
121 |     const invocation: AiActionInvocation = {
122 |       sys: {
123 |         id: "inv1",
124 |         type: "AiActionInvocation",
125 |         space: { sys: { id: "space1", linkType: "Space", type: "Link" } },
126 |         environment: { sys: { id: "master", linkType: "Environment", type: "Link" } },
127 |         aiAction: { sys: { id: "action1", linkType: "AiAction", type: "Link" } },
128 |         status: "COMPLETED"
129 |       },
130 |       result: {
131 |         type: "text",
132 |         content: "Generated content",
133 |         metadata: {
134 |           invocationResult: {
135 |             aiAction: {
136 |               sys: {
137 |                 id: "action1",
138 |                 linkType: "AiAction",
139 |                 type: "Link",
140 |                 version: 1
141 |               }
142 |             },
143 |             outputFormat: "PlainText",
144 |             promptTokens: 50,
145 |             completionTokens: 100,
146 |             modelId: "gpt-4",
147 |             modelProvider: "OpenAI"
148 |           }
149 |         }
150 |       }
151 |     }
152 | 
153 |     expect(invocation.sys.status).toBe("COMPLETED")
154 |     expect(invocation.result?.content).toBe("Generated content")
155 |     expect(invocation.result?.metadata.invocationResult.promptTokens).toBe(50)
156 |   })
157 | })
```

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

```typescript
  1 | import { expect, vi, describe, it, beforeEach } from "vitest"
  2 | import { entryHandlers } from "../../src/handlers/entry-handlers.js"
  3 | 
  4 | // Define test constants 
  5 | const TEST_ENTRY_ID = "test-entry-id"
  6 | const TEST_SPACE_ID = "test-space-id"
  7 | const TEST_ENV_ID = "master"
  8 | 
  9 | // Move vi.mock call to top level - this gets hoisted automatically
 10 | vi.mock("../../src/config/client.js", () => {
 11 |   return {
 12 |     getContentfulClient: vi.fn().mockImplementation(() => {
 13 |       // Create mock entry when the function is called
 14 |       const mockEntry = {
 15 |         sys: { id: TEST_ENTRY_ID, version: 1 },
 16 |         fields: {
 17 |           title: { "en-US": "Original Title" },
 18 |           description: { "en-US": "Original Description" },
 19 |           tags: { "en-US": ["tag1", "tag2"] }
 20 |         }
 21 |       }
 22 | 
 23 |       return {
 24 |         entry: {
 25 |           get: vi.fn().mockResolvedValue(mockEntry),
 26 |           update: vi.fn().mockImplementation((params, entryProps) => {
 27 |             // Return a merged entry that simulates the updated fields
 28 |             return Promise.resolve({
 29 |               sys: { id: params.entryId, version: 2 },
 30 |               fields: entryProps.fields
 31 |             })
 32 |           })
 33 |         }
 34 |       }
 35 |     })
 36 |   }
 37 | })
 38 | 
 39 | describe("Entry Handler Merge Logic", () => {
 40 |   beforeEach(() => {
 41 |     vi.clearAllMocks()
 42 |   })
 43 | 
 44 |   it("should merge existing fields with update fields when only partial update is provided", async () => {
 45 |     // Setup - just update the title but not other fields
 46 |     const updateData = {
 47 |       spaceId: TEST_SPACE_ID,
 48 |       environmentId: TEST_ENV_ID,
 49 |       entryId: TEST_ENTRY_ID,
 50 |       fields: {
 51 |         title: { "en-US": "Updated Title" }
 52 |       }
 53 |     }
 54 | 
 55 |     // Execute
 56 |     const result = await entryHandlers.updateEntry(updateData)
 57 |     
 58 |     // Parse the result
 59 |     const updatedEntry = JSON.parse(result.content[0].text)
 60 |     
 61 |     // Assert - should have updated title but kept original description and tags
 62 |     expect(updatedEntry.fields.title["en-US"]).toEqual("Updated Title")
 63 |     expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description")
 64 |     expect(updatedEntry.fields.tags["en-US"]).toEqual(["tag1", "tag2"])
 65 |   })
 66 | 
 67 |   it("should handle updates to nested locale fields", async () => {
 68 |     // Setup - update a specific locale but not others
 69 |     const updateData = {
 70 |       spaceId: TEST_SPACE_ID,
 71 |       environmentId: TEST_ENV_ID,
 72 |       entryId: TEST_ENTRY_ID,
 73 |       fields: {
 74 |         title: { 
 75 |           "de-DE": "Deutscher Titel" // Add a new locale
 76 |         }
 77 |       }
 78 |     }
 79 | 
 80 |     // Execute
 81 |     const result = await entryHandlers.updateEntry(updateData)
 82 |     
 83 |     // Parse the result
 84 |     const updatedEntry = JSON.parse(result.content[0].text)
 85 |     
 86 |     // Assert - should merge the locales in the title field
 87 |     expect(updatedEntry.fields.title["en-US"]).toEqual("Original Title") // Kept original locale
 88 |     expect(updatedEntry.fields.title["de-DE"]).toEqual("Deutscher Titel") // Added new locale
 89 |     expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description") // Kept other fields
 90 |   })
 91 | 
 92 |   it("should handle adding a new field", async () => {
 93 |     // Setup - add a completely new field
 94 |     const updateData = {
 95 |       spaceId: TEST_SPACE_ID,
 96 |       environmentId: TEST_ENV_ID,
 97 |       entryId: TEST_ENTRY_ID,
 98 |       fields: {
 99 |         newField: { "en-US": "New Field Value" }
100 |       }
101 |     }
102 | 
103 |     // Execute
104 |     const result = await entryHandlers.updateEntry(updateData)
105 |     
106 |     // Parse the result
107 |     const updatedEntry = JSON.parse(result.content[0].text)
108 |     
109 |     // Assert - should have original fields plus the new field
110 |     expect(updatedEntry.fields.title["en-US"]).toEqual("Original Title")
111 |     expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description")
112 |     expect(updatedEntry.fields.newField["en-US"]).toEqual("New Field Value")
113 |   })
114 | 
115 |   it("should handle updating an array field", async () => {
116 |     // Setup - update the tags array
117 |     const updateData = {
118 |       spaceId: TEST_SPACE_ID,
119 |       environmentId: TEST_ENV_ID,
120 |       entryId: TEST_ENTRY_ID,
121 |       fields: {
122 |         tags: { "en-US": ["tag1", "tag2", "tag3"] } // Add tag3
123 |       }
124 |     }
125 | 
126 |     // Execute
127 |     const result = await entryHandlers.updateEntry(updateData)
128 |     
129 |     // Parse the result
130 |     const updatedEntry = JSON.parse(result.content[0].text)
131 |     
132 |     // Assert - should have updated tags but kept other fields
133 |     expect(updatedEntry.fields.title["en-US"]).toEqual("Original Title")
134 |     expect(updatedEntry.fields.description["en-US"]).toEqual("Original Description")
135 |     expect(updatedEntry.fields.tags["en-US"]).toEqual(["tag1", "tag2", "tag3"])
136 |   })
137 | })
```

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

```typescript
  1 | /**
  2 |  * Type definitions for Contentful AI Actions
  3 |  */
  4 | 
  5 | // Variable types
  6 | export type VariableType =
  7 |   | "ResourceLink"
  8 |   | "Text"
  9 |   | "StringOptionsList"
 10 |   | "FreeFormInput"
 11 |   | "StandardInput"
 12 |   | "Locale"
 13 |   | "MediaReference"
 14 |   | "Reference"
 15 |   | "SmartContext";
 16 | 
 17 | // Entity type for reference variables
 18 | export type EntityType = "Entry" | "Asset" | "ResourceLink";
 19 | 
 20 | // Variable configuration types
 21 | export type StringOptionsListConfiguration = {
 22 |   allowFreeFormInput?: boolean;
 23 |   values: string[];
 24 | };
 25 | 
 26 | export type TextConfiguration = {
 27 |   strict: boolean;
 28 |   in: string[];
 29 | };
 30 | 
 31 | export type ReferenceConfiguration = {
 32 |   allowedEntities: EntityType[];
 33 | };
 34 | 
 35 | export type VariableConfiguration =
 36 |   | StringOptionsListConfiguration
 37 |   | TextConfiguration
 38 |   | ReferenceConfiguration;
 39 | 
 40 | // Variable definition
 41 | export interface Variable {
 42 |   id: string;
 43 |   type: VariableType;
 44 |   name?: string;
 45 |   description?: string;
 46 |   configuration?: VariableConfiguration;
 47 | }
 48 | 
 49 | // Condition for conditional template sections
 50 | export type ConditionOperator = "eq" | "neq" | "in" | "nin";
 51 | 
 52 | export interface StringCondition {
 53 |   id: string;
 54 |   variable: string;
 55 |   operator: "eq" | "neq";
 56 |   value: string;
 57 | }
 58 | 
 59 | export interface ArrayCondition {
 60 |   id: string;
 61 |   variable: string;
 62 |   operator: "in" | "nin";
 63 |   value: string[];
 64 | }
 65 | 
 66 | export type Condition = StringCondition | ArrayCondition;
 67 | 
 68 | // Instruction that contains the template and variables
 69 | export interface Instruction {
 70 |   template: string;
 71 |   variables: Variable[];
 72 |   conditions?: Condition[];
 73 | }
 74 | 
 75 | // Model configuration
 76 | export interface Configuration {
 77 |   modelType: string;
 78 |   modelTemperature: number;
 79 | }
 80 | 
 81 | // Input variable value types
 82 | export interface TextVariableValue {
 83 |   id: string;
 84 |   value: string;
 85 | }
 86 | 
 87 | export interface ReferenceVariableValue {
 88 |   id: string;
 89 |   value: {
 90 |     entityType: EntityType;
 91 |     entityId: string;
 92 |     entityPath?: string;
 93 |     entityPaths?: string[];
 94 |   };
 95 | }
 96 | 
 97 | export type VariableValue = TextVariableValue | ReferenceVariableValue;
 98 | 
 99 | // Output format for AI Action results
100 | export type OutputFormat = "RichText" | "Markdown" | "PlainText";
101 | 
102 | // Invocation request
103 | export interface AiActionInvocationType {
104 |   outputFormat?: OutputFormat;
105 |   variables?: VariableValue[];
106 | }
107 | 
108 | // AI Action test case
109 | export interface TextTestCase {
110 |   type: "Text";
111 |   value: string;
112 | }
113 | 
114 | export interface ReferenceTestCase {
115 |   type: "Reference";
116 |   value: {
117 |     entityType: EntityType;
118 |     entityId: string;
119 |     entityPath?: string;
120 |     entityPaths?: string[];
121 |   };
122 | }
123 | 
124 | export type AiActionTestCase = TextTestCase | ReferenceTestCase;
125 | 
126 | // Status for invocations and filtering
127 | export type InvocationStatus = "SCHEDULED" | "IN_PROGRESS" | "FAILED" | "COMPLETED" | "CANCELLED";
128 | export type StatusFilter = "all" | "published";
129 | 
130 | // System links
131 | export interface SysLink {
132 |   sys: {
133 |     id: string;
134 |     linkType: string;
135 |     type: "Link";
136 |   };
137 | }
138 | 
139 | export interface VersionedLink extends SysLink {
140 |   sys: {
141 |     id: string;
142 |     linkType: string;
143 |     type: "Link";
144 |     version: number;
145 |   };
146 | }
147 | 
148 | // AI Action entity
149 | export interface AiActionEntity {
150 |   sys: {
151 |     id: string;
152 |     type: "AiAction";
153 |     createdAt: string;
154 |     updatedAt: string;
155 |     version: number;
156 |     space: SysLink;
157 |     createdBy: SysLink;
158 |     updatedBy: SysLink;
159 |     publishedAt?: string;
160 |     publishedVersion?: number;
161 |     publishedBy?: SysLink;
162 |   };
163 |   name: string;
164 |   description: string;
165 |   instruction: Instruction;
166 |   configuration: Configuration;
167 |   testCases?: AiActionTestCase[];
168 | }
169 | 
170 | export interface AiActionEntityCollection {
171 |   sys: {
172 |     type: "Array";
173 |   };
174 |   items: AiActionEntity[];
175 |   skip?: number;
176 |   limit?: number;
177 |   total?: number;
178 | }
179 | 
180 | // AI Action creation/update schema
181 | export interface AiActionSchemaParsed {
182 |   name: string;
183 |   description: string;
184 |   instruction: Instruction;
185 |   configuration: Configuration;
186 |   testCases?: AiActionTestCase[];
187 | }
188 | 
189 | // Rich text components
190 | export interface Mark {
191 |   type: string;
192 | }
193 | 
194 | export interface Text {
195 |   nodeType: "text";
196 |   value: string;
197 |   marks: Mark[];
198 |   data: Record<string, unknown>;
199 | }
200 | 
201 | export interface Node {
202 |   nodeType: string;
203 |   data: Record<string, unknown>;
204 |   content: (Node | Text)[];
205 | }
206 | 
207 | export interface RichTextDocument {
208 |   nodeType: "document";
209 |   data: Record<string, unknown>;
210 |   content: Node[];
211 | }
212 | 
213 | // Result types
214 | export interface AiActionInvocationMetadata {
215 |   invocationResult: {
216 |     aiAction: VersionedLink;
217 |     outputFormat: OutputFormat;
218 |     promptTokens: number;
219 |     completionTokens: number;
220 |     modelId: string;
221 |     modelProvider: string;
222 |     outputMetadata?: {
223 |       customNodesMap: Record<string, Node>;
224 |     };
225 |   };
226 |   statusChangedDates?: Array<{
227 |     date: string;
228 |     status: InvocationStatus;
229 |   }>;
230 | }
231 | 
232 | export interface FlowResult {
233 |   type: "text";
234 |   content: string | RichTextDocument;
235 |   metadata: AiActionInvocationMetadata;
236 | }
237 | 
238 | export interface AiActionInvocation {
239 |   sys: {
240 |     id: string;
241 |     type: "AiActionInvocation";
242 |     space: SysLink;
243 |     environment: SysLink;
244 |     aiAction: SysLink;
245 |     status: InvocationStatus;
246 |     errorCode?: string;
247 |   };
248 |   result?: FlowResult;
249 | }
```

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

```typescript
  1 | import { expect } from "vitest"
  2 | import { contentTypeHandlers } from "../../src/handlers/content-type-handlers.js"
  3 | import { server } from "../msw-setup.js"
  4 | import { toCamelCase } from "../../src/utils/to-camel-case.js"
  5 | 
  6 | describe("Content Type Handlers Integration Tests", () => {
  7 |   // Start MSW Server before tests
  8 |   beforeAll(() => server.listen())
  9 |   afterEach(() => server.resetHandlers())
 10 |   afterAll(() => server.close())
 11 | 
 12 |   const testSpaceId = "test-space-id"
 13 |   const testContentTypeId = "test-content-type-id"
 14 | 
 15 |   describe("listContentTypes", () => {
 16 |     it("should list all content types", async () => {
 17 |       const result = await contentTypeHandlers.listContentTypes({
 18 |         spaceId: testSpaceId,
 19 |       })
 20 | 
 21 |       expect(result).to.have.property("content").that.is.an("array")
 22 |       expect(result.content).to.have.lengthOf(1)
 23 | 
 24 |       const contentTypes = JSON.parse(result.content[0].text)
 25 |       expect(contentTypes.items).to.be.an("array")
 26 |       expect(contentTypes.items[0]).to.have.nested.property("sys.id", "test-content-type-id")
 27 |       expect(contentTypes.items[0]).to.have.property("name", "Test Content Type")
 28 |     })
 29 |   })
 30 | 
 31 |   describe("getContentType", () => {
 32 |     it("should get details of a specific content type", async () => {
 33 |       const result = await contentTypeHandlers.getContentType({
 34 |         spaceId: testSpaceId,
 35 |         contentTypeId: testContentTypeId,
 36 |       })
 37 | 
 38 |       expect(result).to.have.property("content")
 39 |       const contentType = JSON.parse(result.content[0].text)
 40 |       expect(contentType).to.have.nested.property("sys.id", toCamelCase(testContentTypeId))
 41 |       expect(contentType).to.have.property("name", "Test Content Type")
 42 |       expect(contentType.fields).to.be.an("array")
 43 |     })
 44 | 
 45 |     it("should throw error for invalid content type ID", async () => {
 46 |       try {
 47 |         await contentTypeHandlers.getContentType({
 48 |           spaceId: testSpaceId,
 49 |           contentTypeId: "invalid-id",
 50 |         })
 51 |         expect.fail("Should have thrown an error")
 52 |       } catch (error) {
 53 |         expect(error).to.exist
 54 |       }
 55 |     })
 56 |   })
 57 | 
 58 |   describe("createContentType", () => {
 59 |     it("should create a new content type", async () => {
 60 |       const contentTypeData = {
 61 |         spaceId: testSpaceId,
 62 |         name: "New Content Type",
 63 |         fields: [
 64 |           {
 65 |             id: "title",
 66 |             name: "Title",
 67 |             type: "Text",
 68 |             required: true,
 69 |           },
 70 |         ],
 71 |       }
 72 | 
 73 |       const result = await contentTypeHandlers.createContentType(contentTypeData)
 74 | 
 75 |       expect(result).to.have.property("content")
 76 |       const contentType = JSON.parse(result.content[0].text)
 77 |       expect(contentType).to.have.nested.property("sys.id", "newContentType")
 78 |       expect(contentType).to.have.property("name", "New Content Type")
 79 |       expect(contentType.fields).to.be.an("array")
 80 |     })
 81 |   })
 82 | 
 83 |   describe("updateContentType", () => {
 84 |     it("should update an existing content type", async () => {
 85 |       const updateData = {
 86 |         spaceId: testSpaceId,
 87 |         contentTypeId: testContentTypeId,
 88 |         name: "Updated Content Type",
 89 |         fields: [
 90 |           {
 91 |             id: "title",
 92 |             name: "Updated Title",
 93 |             type: "Text",
 94 |             required: true,
 95 |           },
 96 |         ],
 97 |       }
 98 | 
 99 |       const result = await contentTypeHandlers.updateContentType(updateData)
100 | 
101 |       expect(result).to.have.property("content")
102 |       const contentType = JSON.parse(result.content[0].text)
103 |       expect(contentType).to.have.nested.property("sys.id", testContentTypeId)
104 |       expect(contentType).to.have.property("name", "Updated Content Type")
105 |     })
106 |   })
107 | 
108 |   describe("deleteContentType", () => {
109 |     it("should delete a content type", async () => {
110 |       const result = await contentTypeHandlers.deleteContentType({
111 |         spaceId: testSpaceId,
112 |         contentTypeId: testContentTypeId,
113 |       })
114 | 
115 |       expect(result).to.have.property("content")
116 |       expect(result.content[0].text).to.include("deleted successfully")
117 |     })
118 | 
119 |     it("should throw error when deleting non-existent content type", async () => {
120 |       try {
121 |         await contentTypeHandlers.deleteContentType({
122 |           spaceId: testSpaceId,
123 |           contentTypeId: "non-existent-id",
124 |         })
125 |         expect.fail("Should have thrown an error")
126 |       } catch (error) {
127 |         expect(error).to.exist
128 |       }
129 |     })
130 |   })
131 | 
132 |   describe("publishContentType", () => {
133 |     it("should publish a content type", async () => {
134 |       const result = await contentTypeHandlers.publishContentType({
135 |         spaceId: testSpaceId,
136 |         contentTypeId: testContentTypeId,
137 |       })
138 | 
139 |       expect(result).to.have.property("content")
140 |       expect(result.content[0].text).to.include("published successfully")
141 |     })
142 | 
143 |     it("should throw error when publishing non-existent content type", async () => {
144 |       try {
145 |         await contentTypeHandlers.publishContentType({
146 |           spaceId: testSpaceId,
147 |           contentTypeId: "non-existent-id",
148 |         })
149 |         expect.fail("Should have thrown an error")
150 |       } catch (error) {
151 |         expect(error).to.exist
152 |       }
153 |     })
154 |   })
155 | })
156 | 
```

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

```typescript
  1 | import { expect } from "vitest";
  2 | import { spaceHandlers } from "../../src/handlers/space-handlers.js";
  3 | import { server } from "../msw-setup.js";
  4 | 
  5 | describe("Space Handlers Integration Tests", () => {
  6 |   // Store spaceId for use in other tests
  7 |   let testSpaceId: string;
  8 | 
  9 |   // Start MSW Server before tests
 10 |   beforeAll(() => server.listen());
 11 |   afterEach(() => server.resetHandlers());
 12 |   afterAll(() => server.close());
 13 | 
 14 |   describe("listSpaces", () => {
 15 |     it("should list all available spaces", async () => {
 16 |       const result = await spaceHandlers.listSpaces();
 17 |       expect(result).to.have.property("content");
 18 |       expect(result.content[0]).to.have.property("type", "text");
 19 | 
 20 |       const spaces = JSON.parse(result.content[0].text);
 21 |       expect(spaces).to.have.property("items");
 22 |       expect(Array.isArray(spaces.items)).to.be.true;
 23 | 
 24 |       // Store the first space ID for subsequent tests
 25 |       if (spaces.items.length > 0) {
 26 |         testSpaceId = spaces.items[0].sys.id;
 27 |       }
 28 |     });
 29 |   });
 30 | 
 31 |   describe("getSpace", () => {
 32 |     it("should get details of a specific space", async () => {
 33 |       // Skip if no test space is available
 34 |       if (!testSpaceId) {
 35 |         return;
 36 |       }
 37 | 
 38 |       const result = await spaceHandlers.getSpace({ spaceId: testSpaceId });
 39 |       expect(result).to.have.property("content");
 40 |       expect(result.content[0]).to.have.property("type", "text");
 41 | 
 42 |       const spaceDetails = JSON.parse(result.content[0].text);
 43 |       expect(spaceDetails).to.have.property("sys");
 44 |       expect(spaceDetails.sys).to.have.property("id", testSpaceId);
 45 |     });
 46 | 
 47 |     it("should throw error for invalid space ID", async () => {
 48 |       try {
 49 |         await spaceHandlers.getSpace({ spaceId: "invalid-space-id" });
 50 |         expect.fail("Should have thrown an error");
 51 |       } catch (error) {
 52 |         expect(error).to.exist;
 53 |       }
 54 |     });
 55 |   });
 56 | 
 57 |   describe("listEnvironments", () => {
 58 |     it("should list environments for a space", async () => {
 59 |       // Skip if no test space is available
 60 |       if (!testSpaceId) {
 61 |         return;
 62 |       }
 63 | 
 64 |       const result = await spaceHandlers.listEnvironments({
 65 |         spaceId: testSpaceId,
 66 |       });
 67 |       expect(result).to.have.property("content");
 68 |       expect(result.content[0]).to.have.property("type", "text");
 69 | 
 70 |       const environments = JSON.parse(result.content[0].text);
 71 |       expect(environments).to.have.property("items");
 72 |       expect(Array.isArray(environments.items)).to.be.true;
 73 | 
 74 |       // Verify master environment exists
 75 |       const masterEnv = environments.items.find(
 76 |         (env: any) => env.sys.id === "master",
 77 |       );
 78 |       expect(masterEnv).to.exist;
 79 |     });
 80 | 
 81 |     it("should throw error for invalid space ID", async () => {
 82 |       try {
 83 |         await spaceHandlers.listEnvironments({ spaceId: "invalid-space-id" });
 84 |         expect.fail("Should have thrown an error");
 85 |       } catch (error) {
 86 |         expect(error).to.exist;
 87 |       }
 88 |     });
 89 |   });
 90 | 
 91 |   describe("createEnvironment", () => {
 92 |     it("should create a new environment", async () => {
 93 |       // Skip if no test space is available
 94 |       if (!testSpaceId) {
 95 |         return;
 96 |       }
 97 | 
 98 |       const envName = `test-env-${Date.now()}`;
 99 |       const result = await spaceHandlers.createEnvironment({
100 |         spaceId: testSpaceId,
101 |         environmentId: envName,
102 |         name: envName,
103 |       });
104 | 
105 |       expect(result).to.have.property("content");
106 |       expect(result.content[0]).to.have.property("type", "text");
107 | 
108 |       const environment = JSON.parse(result.content[0].text);
109 |       expect(environment).to.have.property("sys");
110 |       expect(environment.sys).to.have.property("id", envName);
111 |       expect(environment).to.have.property("name", envName);
112 | 
113 |       // Store environment ID for deletion test
114 |       return envName;
115 |     });
116 | 
117 |     it("should throw error for invalid space ID", async () => {
118 |       try {
119 |         await spaceHandlers.createEnvironment({
120 |           spaceId: "invalid-space-id",
121 |           environmentId: "test-env",
122 |           name: "Test Environment",
123 |         });
124 |         expect.fail("Should have thrown an error");
125 |       } catch (error) {
126 |         expect(error).to.exist;
127 |       }
128 |     });
129 |   });
130 | 
131 |   describe("deleteEnvironment", () => {
132 |     it("should delete an environment", async () => {
133 |       // Skip if no test space is available
134 |       if (!testSpaceId) {
135 |         return;
136 |       }
137 | 
138 |       // Create a temporary environment
139 |       const envName = `temp-env-${Date.now()}`;
140 |       await spaceHandlers.createEnvironment({
141 |         spaceId: testSpaceId,
142 |         environmentId: envName,
143 |         name: envName,
144 |       });
145 | 
146 |       // Delete the environment
147 |       const result = await spaceHandlers.deleteEnvironment({
148 |         spaceId: testSpaceId,
149 |         environmentId: envName,
150 |       });
151 | 
152 |       expect(result).to.have.property("content");
153 |       expect(result.content[0]).to.have.property("type", "text");
154 |       expect(result.content[0].text).to.include("deleted successfully");
155 |     });
156 | 
157 |     it("should throw error for invalid environment ID", async () => {
158 |       if (!testSpaceId) {
159 |         return;
160 |       }
161 | 
162 |       try {
163 |         await spaceHandlers.deleteEnvironment({
164 |           spaceId: testSpaceId,
165 |           environmentId: "non-existent-env",
166 |         });
167 |         expect.fail("Should have thrown an error");
168 |       } catch (error) {
169 |         expect(error).to.exist;
170 |       }
171 |     });
172 |   });
173 | });
174 | 
```

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

```typescript
  1 | /**
  2 |  * Handler for invoking AI Actions in Contentful
  3 |  */
  4 | export async function handleAiActionsInvoke(actionId?: string, details?: string) {
  5 |   return {
  6 |     messages: [
  7 |       {
  8 |         role: "assistant",
  9 |         content: {
 10 |           type: "text",
 11 |           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.",
 12 |         },
 13 |       },
 14 |       {
 15 |         role: "user",
 16 |         content: {
 17 |           type: "text",
 18 |           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."}`
 19 |         },
 20 |       },
 21 |       {
 22 |         role: "assistant",
 23 |         content: {
 24 |           type: "text",
 25 |           text: `# Invoking AI Actions in Contentful${actionId ? `: ${actionId}` : ""}
 26 | 
 27 | I'll guide you through the process of effectively invoking AI Actions through the MCP interface.
 28 | 
 29 | ## Finding Available AI Actions
 30 | 
 31 | Before invoking an AI Action, you need to know which ones are available in your space. You can discover them using:
 32 | 
 33 | \`\`\`javascript
 34 | list_ai_actions({
 35 |   spaceId: "your-space-id",
 36 |   environmentId: "master", // typically "master" or your environment name
 37 |   status: "published" // only show published (available) actions
 38 | });
 39 | \`\`\`
 40 | 
 41 | This will return a list of AI Actions with their IDs, names, descriptions, and other metadata.
 42 | 
 43 | ## Understanding An AI Action's Requirements
 44 | 
 45 | Once you've identified the AI Action you want to use${actionId ? ` (in this case, ${actionId})` : ""}, you can get its details:
 46 | 
 47 | \`\`\`javascript
 48 | get_ai_action({
 49 |   spaceId: "your-space-id",
 50 |   environmentId: "master",
 51 |   aiActionId: ${actionId ? `"${actionId}"` : `"the-action-id"`}
 52 | });
 53 | \`\`\`
 54 | 
 55 | This will show you the full definition, including all required variables and their types.
 56 | 
 57 | ## Dynamic AI Action Tools
 58 | 
 59 | 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:
 60 | 
 61 | \`\`\`javascript
 62 | ai_action_3woPNtzC81CEsBEvgQo96J({
 63 |   // parameters based on the AI Action's variables
 64 | });
 65 | \`\`\`
 66 | 
 67 | ## Preparing Parameters
 68 | 
 69 | AI Actions require specific parameters based on their variable definitions:
 70 | 
 71 | ### Basic Variable Types
 72 | 
 73 | For simple variable types (Text, FreeFormInput, StringOptionsList, Locale), provide the values directly:
 74 | 
 75 | \`\`\`javascript
 76 | ai_action_example({
 77 |   tone: "Professional", // StringOptionsList
 78 |   target_audience: "Enterprise customers", // Text
 79 |   locale: "en-US" // Locale
 80 | });
 81 | \`\`\`
 82 | 
 83 | ### Reference Variables
 84 | 
 85 | For Reference variables (linking to other entries), you need to provide:
 86 | 
 87 | 1. The entry ID
 88 | 2. The field path to access
 89 | 
 90 | \`\`\`javascript
 91 | ai_action_example({
 92 |   product_entry: "6tFnSQdgHuWYOk8eICA0w", // Entry ID
 93 |   product_entry_path: "fields.description.en-US" // Field path
 94 | });
 95 | \`\`\`
 96 | 
 97 | ### Media Reference Variables
 98 | 
 99 | Similarly, for MediaReference variables (images, videos, etc.):
100 | 
101 | \`\`\`javascript
102 | ai_action_example({
103 |   product_image: "7tGnRQegIvWZPj9eICA1q", // Asset ID
104 |   product_image_path: "fields.file.en-US" // Field path
105 | });
106 | \`\`\`
107 | 
108 | ### Standard Input
109 | 
110 | For the main content (StandardInput):
111 | 
112 | \`\`\`javascript
113 | ai_action_example({
114 |   input_text: "Your content to process..."
115 | });
116 | \`\`\`
117 | 
118 | ## Complete Example
119 | 
120 | Here's a complete example of invoking an AI Action:
121 | 
122 | \`\`\`javascript
123 | ai_action_content_enhancer({
124 |   // Basic parameters
125 |   input_text: "Original content to enhance...",
126 |   tone: "Professional",
127 |   
128 |   // Reference to another entry
129 |   brand_guidelines: "1aBcDeFgHiJkLmNoPqR",
130 |   brand_guidelines_path: "fields.guidelines.en-US",
131 |   
132 |   // Additional settings
133 |   outputFormat: "Markdown", // Output format (Markdown, RichText, or PlainText)
134 |   waitForCompletion: true // Wait for processing to complete
135 | });
136 | \`\`\`
137 | 
138 | ## Output Formats
139 | 
140 | You can specify how you want the output formatted using the \`outputFormat\` parameter:
141 | 
142 | - **Markdown**: Clean, formatted text with markdown syntax (default)
143 | - **RichText**: Contentful's structured rich text format
144 | - **PlainText**: Simple text without formatting
145 | 
146 | ## Asynchronous Processing
147 | 
148 | AI Actions process asynchronously by default. You can control this behavior with:
149 | 
150 | - **waitForCompletion**: Set to \`true\` to wait for the operation to complete (default)
151 | - If set to \`false\`, you'll receive an invocation ID that you can use to check status later
152 | 
153 | ## Getting Results Later
154 | 
155 | If you opted not to wait for completion, you can check the status later:
156 | 
157 | \`\`\`javascript
158 | get_ai_action_invocation({
159 |   spaceId: "your-space-id",
160 |   environmentId: "master",
161 |   aiActionId: "the-action-id",
162 |   invocationId: "the-invocation-id" // Received from the initial invoke call
163 | });
164 | \`\`\`
165 | 
166 | ## Important Notes
167 | 
168 | 1. **Results are not automatically applied**: AI Action results are returned for you to review but aren't automatically applied to entries.
169 | 
170 | 2. **Field paths are crucial**: When working with References and MediaReferences, always provide the correct field path.
171 | 
172 | 3. **Check for required variables**: All required variables must be provided, or the invocation will fail.
173 | 
174 | 4. **Response times vary**: Complex AI Actions may take longer to process.
175 | 
176 | Does this help with your AI Action invocation? Would you like more specific guidance on any of these aspects?`,
177 |         },
178 |       },
179 |     ],
180 |   };
181 | }
```

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

```typescript
  1 | /**
  2 |  * Prompt definitions for the Contentful MCP server
  3 |  * These prompts help guide users through common operations and concepts
  4 |  */
  5 | export const CONTENTFUL_PROMPTS = {
  6 |   "explain-api-concepts": {
  7 |     name: "explain-api-concepts",
  8 |     description: "Explain Contentful API concepts and relationships",
  9 |     arguments: [
 10 |       {
 11 |         name: "concept",
 12 |         description: "Contentful concept (Space/Environment/ContentType/Entry/Asset)",
 13 |         required: true
 14 |       }
 15 |     ]
 16 |   },
 17 |   "space-identification": {
 18 |     name: "space-identification",
 19 |     description: "Guide for identifying the correct Contentful space for operations",
 20 |     arguments: [
 21 |       {
 22 |         name: "operation",
 23 |         description: "Operation you want to perform",
 24 |         required: true
 25 |       }
 26 |     ]
 27 |   },
 28 |   "content-modeling-guide": {
 29 |     name: "content-modeling-guide",
 30 |     description: "Guide through content modeling decisions and best practices",
 31 |     arguments: [
 32 |       {
 33 |         name: "useCase",
 34 |         description: "Description of the content modeling scenario",
 35 |         required: true
 36 |       }
 37 |     ]
 38 |   },
 39 |   "api-operation-help": {
 40 |     name: "api-operation-help",
 41 |     description: "Get detailed help for specific Contentful API operations",
 42 |     arguments: [
 43 |       {
 44 |         name: "operation",
 45 |         description: "API operation (CRUD, publish, archive, etc)",
 46 |         required: true
 47 |       },
 48 |       {
 49 |         name: "resourceType",
 50 |         description: "Type of resource (Entry/Asset/ContentType)",
 51 |         required: true
 52 |       }
 53 |     ]
 54 |   },
 55 |   "entry-management": {
 56 |     name: "entry-management",
 57 |     description: "Help with CRUD operations and publishing workflows for content entries",
 58 |     arguments: [
 59 |       {
 60 |         name: "task",
 61 |         description: "Specific task (create/read/update/delete/publish/unpublish/bulk)",
 62 |         required: false
 63 |       },
 64 |       {
 65 |         name: "details",
 66 |         description: "Additional context or requirements",
 67 |         required: false
 68 |       }
 69 |     ]
 70 |   },
 71 |   "asset-management": {
 72 |     name: "asset-management",
 73 |     description: "Guidance on managing digital assets like images, videos, and documents",
 74 |     arguments: [
 75 |       {
 76 |         name: "task",
 77 |         description: "Specific task (upload/process/update/delete/publish)",
 78 |         required: false
 79 |       },
 80 |       {
 81 |         name: "details",
 82 |         description: "Additional context about asset types or requirements",
 83 |         required: false
 84 |       }
 85 |     ]
 86 |   },
 87 |   "content-type-operations": {
 88 |     name: "content-type-operations",
 89 |     description: "Help with defining and managing content types and their fields",
 90 |     arguments: [
 91 |       {
 92 |         name: "task",
 93 |         description: "Specific task (create/update/delete/publish/field configuration)",
 94 |         required: false
 95 |       },
 96 |       {
 97 |         name: "details",
 98 |         description: "Additional context about field types or validations",
 99 |         required: false
100 |       }
101 |     ]
102 |   },
103 |   "ai-actions-overview": {
104 |     name: "ai-actions-overview",
105 |     description: "Comprehensive overview of AI Actions in Contentful",
106 |     arguments: []
107 |   },
108 |   "ai-actions-create": {
109 |     name: "ai-actions-create",
110 |     description: "Guide for creating and configuring AI Actions in Contentful",
111 |     arguments: [
112 |       {
113 |         name: "useCase",
114 |         description: "Purpose of the AI Action you want to create",
115 |         required: true
116 |       },
117 |       {
118 |         name: "modelType",
119 |         description: "AI model type (e.g., gpt-4, claude-3-opus)",
120 |         required: false
121 |       }
122 |     ]
123 |   },
124 |   "ai-actions-variables": {
125 |     name: "ai-actions-variables",
126 |     description: "Explanation of variable types and configuration for AI Actions",
127 |     arguments: [
128 |       {
129 |         name: "variableType",
130 |         description: "Type of variable (Text, Reference, StandardInput, etc)",
131 |         required: false
132 |       }
133 |     ]
134 |   },
135 |   "ai-actions-invoke": {
136 |     name: "ai-actions-invoke",
137 |     description: "Help with invoking AI Actions and processing results",
138 |     arguments: [
139 |       {
140 |         name: "actionId",
141 |         description: "ID of the AI Action (if known)",
142 |         required: false
143 |       },
144 |       {
145 |         name: "details",
146 |         description: "Additional context about your invocation requirements",
147 |         required: false
148 |       }
149 |     ]
150 |   },
151 |   "bulk-operations": {
152 |     name: "bulk-operations",
153 |     description: "Guidance on performing actions on multiple entities simultaneously",
154 |     arguments: [
155 |       {
156 |         name: "operation",
157 |         description: "Bulk operation type (publish/unpublish/validate)",
158 |         required: false
159 |       },
160 |       {
161 |         name: "entityType",
162 |         description: "Type of entities to process (entries/assets)",
163 |         required: false
164 |       },
165 |       {
166 |         name: "details",
167 |         description: "Additional context about operation requirements",
168 |         required: false
169 |       }
170 |     ]
171 |   },
172 |   "space-environment-management": {
173 |     name: "space-environment-management",
174 |     description: "Help with managing spaces, environments, and deployment workflows",
175 |     arguments: [
176 |       {
177 |         name: "task",
178 |         description: "Specific task (create/list/manage environments/aliases)",
179 |         required: false
180 |       },
181 |       {
182 |         name: "entity",
183 |         description: "Entity type (space/environment)",
184 |         required: false
185 |       },
186 |       {
187 |         name: "details",
188 |         description: "Additional context about workflow requirements",
189 |         required: false
190 |       }
191 |     ]
192 |   },
193 |   "mcp-tool-usage": {
194 |     name: "mcp-tool-usage",
195 |     description: "Instructions for using Contentful MCP tools effectively",
196 |     arguments: [
197 |       {
198 |         name: "toolName",
199 |         description: "Specific tool name (e.g., invoke_ai_action, create_entry)",
200 |         required: false
201 |       }
202 |     ]
203 |   }
204 | };
```

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

```json
  1 | {
  2 |   "name": "Contentful MCP",
  3 |   "version": "1.0.0",
  4 |   "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.",
  5 |   "vars": {
  6 |     "typescript_path": "src/types",
  7 |     "handlers_path": "src/handlers",
  8 |     "utils_path": "src/utils",
  9 |     "config_path": "src/config"
 10 |   },
 11 |   "groups": [
 12 |     {
 13 |       "name": "Core",
 14 |       "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}",
 15 |       "data": ["index", "ai-actions-client", "client"]
 16 |     },
 17 |     {
 18 |       "name": "Handlers",
 19 |       "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}",
 20 |       "data": [
 21 |         "entry-handlers",
 22 |         "asset-handlers",
 23 |         "content-type-handlers",
 24 |         "ai-action-handlers",
 25 |         "bulk-action-handlers",
 26 |         "space-handlers"
 27 |       ]
 28 |     },
 29 |     {
 30 |       "name": "Types",
 31 |       "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}",
 32 |       "data": ["ai-actions-types", "tools-types"]
 33 |     },
 34 |     {
 35 |       "name": "Utils",
 36 |       "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}",
 37 |       "data": ["ai-action-tool-generator", "summarizer", "validation"]
 38 |     }
 39 |   ],
 40 |   "data": {
 41 |     "index": {
 42 |       "type": "file",
 43 |       "path": "src/index.ts",
 44 |       "description": "The main entry point for the Contentful MCP project where handlers are registered and the server is initialized."
 45 |     },
 46 |     "ai-actions-client": {
 47 |       "type": "file",
 48 |       "path": "${config_path}/ai-actions-client.ts",
 49 |       "description": "Configuration for the AI Actions client used to interact with Contentful's AI Actions API, with specialized authentication and endpoint handling."
 50 |     },
 51 |     "client": {
 52 |       "type": "file",
 53 |       "path": "${config_path}/client.ts",
 54 |       "description": "Configuration for the main Contentful client used to interact with the Contentful API, handling authentication and base configuration."
 55 |     },
 56 |     "entry-handlers": {
 57 |       "type": "file",
 58 |       "path": "${handlers_path}/entry-handlers.ts",
 59 |       "description": "Handlers for CRUD operations and other actions on Contentful entries, including bulk publishing and unpublishing functionality."
 60 |     },
 61 |     "asset-handlers": {
 62 |       "type": "file",
 63 |       "path": "${handlers_path}/asset-handlers.ts",
 64 |       "description": "Handlers for CRUD operations and other actions on Contentful assets, including upload, update and publishing."
 65 |     },
 66 |     "content-type-handlers": {
 67 |       "type": "file",
 68 |       "path": "${handlers_path}/content-type-handlers.ts",
 69 |       "description": "Handlers for CRUD operations and other actions on Contentful content types, defining the structure of entries."
 70 |     },
 71 |     "ai-action-handlers": {
 72 |       "type": "file",
 73 |       "path": "${handlers_path}/ai-action-handlers.ts",
 74 |       "description": "Handlers for CRUD operations and other actions on Contentful AI Actions, including invocation and result retrieval."
 75 |     },
 76 |     "bulk-action-handlers": {
 77 |       "type": "file",
 78 |       "path": "${handlers_path}/bulk-action-handlers.ts",
 79 |       "description": "Handlers for bulk operations on Contentful entities, optimizing performance for operations on multiple items."
 80 |     },
 81 |     "space-handlers": {
 82 |       "type": "file",
 83 |       "path": "${handlers_path}/space-handlers.ts",
 84 |       "description": "Handlers for operations on Contentful spaces, providing top-level organization functionality."
 85 |     },
 86 |     "ai-actions-types": {
 87 |       "type": "file",
 88 |       "path": "${typescript_path}/ai-actions.ts",
 89 |       "description": "Type definitions for AI Actions entities and operations, including configuration, templates, and variables schemas."
 90 |     },
 91 |     "tools-types": {
 92 |       "type": "file",
 93 |       "path": "${typescript_path}/tools.ts",
 94 |       "description": "Type definitions for the MCP tools used in the project, defining the interface between the model and Contentful."
 95 |     },
 96 |     "ai-action-tool-generator": {
 97 |       "type": "file",
 98 |       "path": "${utils_path}/ai-action-tool-generator.ts",
 99 |       "description": "Utility for generating tool configurations for AI Actions, mapping parameters and creating JSON schemas."
100 |     },
101 |     "summarizer": {
102 |       "type": "file",
103 |       "path": "${utils_path}/summarizer.ts",
104 |       "description": "Utility for summarizing content and responses to provide concise information to the model."
105 |     },
106 |     "validation": {
107 |       "type": "file",
108 |       "path": "${utils_path}/validation.ts",
109 |       "description": "Utility for validating input data and responses to ensure data integrity and proper formatting."
110 |     }
111 |   }
112 | }
```

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

```typescript
  1 | /* eslint-disable @typescript-eslint/no-explicit-any */
  2 | import { CreateAssetProps } from "contentful-management"
  3 | import { getContentfulClient } from "../config/client.js"
  4 | 
  5 | type BaseAssetParams = {
  6 |   spaceId: string
  7 |   environmentId: string
  8 |   assetId: string
  9 | }
 10 | 
 11 | const formatResponse = (data: any) => ({
 12 |   content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
 13 | })
 14 | 
 15 | const getCurrentAsset = async (params: BaseAssetParams) => {
 16 |   const contentfulClient = await getContentfulClient()
 17 |   return contentfulClient.asset.get(params)
 18 | }
 19 | 
 20 | import { summarizeData } from "../utils/summarizer.js"
 21 | 
 22 | export const assetHandlers = {
 23 |   listAssets: async (args: {
 24 |     spaceId: string
 25 |     environmentId: string
 26 |     limit: number
 27 |     skip: number
 28 |   }) => {
 29 |     const spaceId = process.env.SPACE_ID || args.spaceId
 30 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 31 | 
 32 |     const params = {
 33 |       spaceId,
 34 |       environmentId,
 35 |       query: {
 36 |         limit: Math.min(args.limit || 3, 3),
 37 |         skip: args.skip || 0,
 38 |       },
 39 |     }
 40 | 
 41 |     const contentfulClient = await getContentfulClient()
 42 |     const assets = await contentfulClient.asset.getMany(params)
 43 | 
 44 |     const summarized = summarizeData(assets, {
 45 |       maxItems: 3,
 46 |       remainingMessage: "To see more assets, please ask me to retrieve the next page.",
 47 |     })
 48 | 
 49 |     return formatResponse(summarized)
 50 |   },
 51 |   uploadAsset: async (args: {
 52 |     spaceId: string
 53 |     environmentId: string
 54 |     title: string
 55 |     description?: string
 56 |     file: {
 57 |       fileName: string
 58 |       contentType: string
 59 |       upload?: string
 60 |     }
 61 |   }) => {
 62 |     const spaceId = process.env.SPACE_ID || args.spaceId
 63 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 64 | 
 65 |     const params = {
 66 |       spaceId,
 67 |       environmentId,
 68 |     }
 69 | 
 70 |     const assetProps: CreateAssetProps = {
 71 |       fields: {
 72 |         title: { "en-US": args.title },
 73 |         description: args.description ? { "en-US": args.description } : undefined,
 74 |         file: { "en-US": args.file },
 75 |       },
 76 |     }
 77 | 
 78 |     const contentfulClient = await getContentfulClient()
 79 |     const asset = await contentfulClient.asset.create(params, assetProps)
 80 | 
 81 |     const processedAsset = await contentfulClient.asset.processForAllLocales(
 82 |       params,
 83 |       {
 84 |         sys: asset.sys,
 85 |         fields: asset.fields,
 86 |       },
 87 |       {},
 88 |     )
 89 | 
 90 |     return formatResponse(processedAsset)
 91 |   },
 92 | 
 93 |   getAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
 94 |     const spaceId = process.env.SPACE_ID || args.spaceId
 95 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 96 | 
 97 |     const params = {
 98 |       spaceId,
 99 |       environmentId,
100 |       assetId: args.assetId,
101 |     }
102 | 
103 |     const contentfulClient = await getContentfulClient()
104 |     const asset = await contentfulClient.asset.get(params)
105 |     return formatResponse(asset)
106 |   },
107 | 
108 |   updateAsset: async (args: {
109 |     spaceId: string
110 |     environmentId: string
111 |     assetId: string
112 |     title?: string
113 |     description?: string
114 |     file?: {
115 |       fileName: string
116 |       contentType: string
117 |       upload?: string
118 |     }
119 |   }) => {
120 |     const spaceId = process.env.SPACE_ID || args.spaceId
121 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
122 | 
123 |     const params = {
124 |       spaceId,
125 |       environmentId,
126 |       assetId: args.assetId,
127 |     }
128 |     const currentAsset = await getCurrentAsset(params)
129 | 
130 |     const fields: Record<string, any> = {}
131 |     if (args.title) fields.title = { "en-US": args.title }
132 |     if (args.description) fields.description = { "en-US": args.description }
133 |     if (args.file) fields.file = { "en-US": args.file }
134 |     const updateParams = {
135 |       fields: {
136 |         title: args.title ? { "en-US": args.title } : currentAsset.fields.title,
137 |         description: args.description
138 |           ? { "en-US": args.description }
139 |           : currentAsset.fields.description,
140 |         file: args.file ? { "en-US": args.file } : currentAsset.fields.file,
141 |       },
142 |       sys: currentAsset.sys,
143 |     }
144 | 
145 |     const contentfulClient = await getContentfulClient()
146 |     const asset = await contentfulClient.asset.update(params, updateParams)
147 | 
148 |     return formatResponse(asset)
149 |   },
150 | 
151 |   deleteAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
152 |     const spaceId = process.env.SPACE_ID || args.spaceId
153 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
154 | 
155 |     const params = {
156 |       spaceId,
157 |       environmentId,
158 |       assetId: args.assetId,
159 |     }
160 |     const currentAsset = await getCurrentAsset(params)
161 | 
162 |     const contentfulClient = await getContentfulClient()
163 |     await contentfulClient.asset.delete({
164 |       ...params,
165 |       version: currentAsset.sys.version,
166 |     })
167 | 
168 |     return formatResponse({
169 |       message: `Asset ${args.assetId} deleted successfully`,
170 |     })
171 |   },
172 | 
173 |   publishAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
174 |     const spaceId = process.env.SPACE_ID || args.spaceId
175 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
176 | 
177 |     const params = {
178 |       spaceId,
179 |       environmentId,
180 |       assetId: args.assetId,
181 |     }
182 |     const currentAsset = await getCurrentAsset(params)
183 | 
184 |     const contentfulClient = await getContentfulClient()
185 |     const asset = await contentfulClient.asset.publish(params, {
186 |       sys: currentAsset.sys,
187 |       fields: currentAsset.fields,
188 |     })
189 | 
190 |     return formatResponse(asset)
191 |   },
192 | 
193 |   unpublishAsset: async (args: { spaceId: string; environmentId: string; assetId: string }) => {
194 |     const spaceId = process.env.SPACE_ID || args.spaceId
195 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
196 | 
197 |     const params = {
198 |       spaceId,
199 |       environmentId,
200 |       assetId: args.assetId,
201 |     }
202 |     const currentAsset = await getCurrentAsset(params)
203 | 
204 |     const contentfulClient = await getContentfulClient()
205 |     const asset = await contentfulClient.asset.unpublish({
206 |       ...params,
207 |       version: currentAsset.sys.version,
208 |     })
209 | 
210 |     return formatResponse(asset)
211 |   },
212 | }
213 | 
```

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

```typescript
  1 | /* eslint-disable @typescript-eslint/no-explicit-any */
  2 | import { getContentfulClient } from "../config/client.js"
  3 | import { ContentTypeProps, CreateContentTypeProps } from "contentful-management"
  4 | import { toCamelCase } from "../utils/to-camel-case.js"
  5 | 
  6 | export const contentTypeHandlers = {
  7 |   listContentTypes: async (args: { spaceId: string; environmentId: string }) => {
  8 |     const spaceId = process.env.SPACE_ID || args.spaceId
  9 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 10 | 
 11 |     const params = {
 12 |       spaceId,
 13 |       environmentId,
 14 |     }
 15 | 
 16 |     const contentfulClient = await getContentfulClient()
 17 |     const contentTypes = await contentfulClient.contentType.getMany(params)
 18 |     return {
 19 |       content: [{ type: "text", text: JSON.stringify(contentTypes, null, 2) }],
 20 |     }
 21 |   },
 22 |   getContentType: async (args: {
 23 |     spaceId: string
 24 |     environmentId: string
 25 |     contentTypeId: string
 26 |   }) => {
 27 |     const spaceId = process.env.SPACE_ID || args.spaceId
 28 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 29 | 
 30 |     const params = {
 31 |       spaceId,
 32 |       environmentId,
 33 |       contentTypeId: args.contentTypeId,
 34 |     }
 35 | 
 36 |     const contentfulClient = await getContentfulClient()
 37 |     const contentType = await contentfulClient.contentType.get(params)
 38 |     return {
 39 |       content: [{ type: "text", text: JSON.stringify(contentType, null, 2) }],
 40 |     }
 41 |   },
 42 | 
 43 |   createContentType: async (args: {
 44 |     spaceId: string
 45 |     environmentId: string
 46 |     name: string
 47 |     fields: any[]
 48 |     description?: string
 49 |     displayField?: string
 50 |   }) => {
 51 |     const spaceId = process.env.SPACE_ID || args.spaceId
 52 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 53 | 
 54 |     const params = {
 55 |       contentTypeId: toCamelCase(args.name),
 56 |       spaceId,
 57 |       environmentId,
 58 |     }
 59 | 
 60 |     const contentTypeProps: CreateContentTypeProps = {
 61 |       name: args.name,
 62 |       fields: args.fields,
 63 |       description: args.description || "",
 64 |       displayField: args.displayField || args.fields[0]?.id || "",
 65 |     }
 66 | 
 67 |     const contentfulClient = await getContentfulClient()
 68 |     const contentType = await contentfulClient.contentType.createWithId(params, contentTypeProps)
 69 | 
 70 |     return {
 71 |       content: [{ type: "text", text: JSON.stringify(contentType, null, 2) }],
 72 |     }
 73 |   },
 74 | 
 75 |   updateContentType: async (args: {
 76 |     spaceId: string
 77 |     environmentId: string
 78 |     contentTypeId: string
 79 |     name?: string
 80 |     fields?: any[]
 81 |     description?: string
 82 |     displayField?: string
 83 |   }) => {
 84 |     const spaceId = process.env.SPACE_ID || args.spaceId
 85 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
 86 | 
 87 |     const params = {
 88 |       spaceId,
 89 |       environmentId,
 90 |       contentTypeId: args.contentTypeId,
 91 |     }
 92 | 
 93 |     const contentfulClient = await getContentfulClient()
 94 |     const currentContentType = await contentfulClient.contentType.get(params)
 95 | 
 96 |     // Use the new fields if provided, otherwise keep existing fields
 97 |     const fields = args.fields || currentContentType.fields
 98 | 
 99 |     // If fields are provided, ensure we're not removing any required field metadata
100 |     // This creates a map of existing fields by ID for easier lookup
101 |     if (args.fields) {
102 |       const existingFieldsMap = currentContentType.fields.reduce((acc: Record<string, any>, field: any) => {
103 |         acc[field.id] = field
104 |         return acc
105 |       }, {})
106 | 
107 |       // Ensure each field has all required metadata
108 |       fields.forEach((field: any) => {
109 |         const existingField = existingFieldsMap[field.id]
110 |         if (existingField) {
111 |           // If this is an existing field, ensure we preserve any metadata not explicitly changed
112 |           // This prevents losing validations, linkType, etc.
113 |           field.validations = field.validations || existingField.validations
114 | 
115 |           // Preserve required flag if not explicitly set
116 |           if (field.required === undefined && existingField.required !== undefined) {
117 |             field.required = existingField.required
118 |           }
119 | 
120 |           if (field.type === 'Link' && !field.linkType && existingField.linkType) {
121 |             field.linkType = existingField.linkType
122 |           }
123 | 
124 |           if (field.type === 'Array' && !field.items && existingField.items) {
125 |             field.items = existingField.items
126 |           }
127 |         }
128 |       })
129 |     }
130 | 
131 |     const contentTypeProps: ContentTypeProps = {
132 |       name: args.name || currentContentType.name,
133 |       fields: fields,
134 |       description: args.description || currentContentType.description || "",
135 |       displayField: args.displayField || currentContentType.displayField || "",
136 |       sys: currentContentType.sys,
137 |     }
138 | 
139 |     const contentType = await contentfulClient.contentType.update(params, contentTypeProps)
140 |     return {
141 |       content: [{ type: "text", text: JSON.stringify(contentType, null, 2) }],
142 |     }
143 |   },
144 | 
145 |   deleteContentType: async (args: {
146 |     spaceId: string
147 |     environmentId: string
148 |     contentTypeId: string
149 |   }) => {
150 |     const spaceId = process.env.SPACE_ID || args.spaceId
151 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
152 | 
153 |     const params = {
154 |       spaceId,
155 |       environmentId,
156 |       contentTypeId: args.contentTypeId,
157 |     }
158 | 
159 |     const contentfulClient = await getContentfulClient()
160 |     await contentfulClient.contentType.delete(params)
161 |     return {
162 |       content: [
163 |         {
164 |           type: "text",
165 |           text: `Content type ${args.contentTypeId} deleted successfully`,
166 |         },
167 |       ],
168 |     }
169 |   },
170 | 
171 |   publishContentType: async (args: {
172 |     spaceId: string
173 |     environmentId: string
174 |     contentTypeId: string
175 |   }) => {
176 |     const spaceId = process.env.SPACE_ID || args.spaceId
177 |     const environmentId = process.env.ENVIRONMENT_ID || args.environmentId
178 | 
179 |     const params = {
180 |       spaceId,
181 |       environmentId,
182 |       contentTypeId: args.contentTypeId,
183 |     }
184 | 
185 |     const contentfulClient = await getContentfulClient()
186 |     const contentType = await contentfulClient.contentType.get(params)
187 |     await contentfulClient.contentType.publish(params, contentType)
188 | 
189 |     return {
190 |       content: [
191 |         {
192 |           type: "text",
193 |           text: `Content type ${args.contentTypeId} published successfully`,
194 |         },
195 |       ],
196 |     }
197 |   },
198 | }
199 | 
```

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

```typescript
  1 | import { GetPromptResult } from "@modelcontextprotocol/sdk/types";
  2 | import { generateVariableTypeContent } from "../generateVariableTypeContent";
  3 | import { handleAiActionsInvoke } from "../ai-actions-invoke";
  4 | import { handleAiActionsOverview } from "../ai-actions-overview";
  5 | 
  6 | /**
  7 |  * Handler for AI Actions create prompt
  8 |  * @param args Optional arguments for the prompt
  9 |  * @returns Prompt result with messages
 10 |  */
 11 | export function handleAiActionsCreate(args?: Record<string, string>): GetPromptResult {
 12 |   return {
 13 |     messages: [
 14 |       {
 15 |         role: "assistant",
 16 |         content: {
 17 |           type: "text",
 18 |           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.",
 19 |         },
 20 |       },
 21 |       {
 22 |         role: "user",
 23 |         content: {
 24 |           type: "text",
 25 |           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.`,
 26 |         },
 27 |       },
 28 |       {
 29 |         role: "assistant",
 30 |         content: {
 31 |           type: "text",
 32 |           text: `# Creating an AI Action for ${args?.useCase || "your Contentful project"}
 33 | 
 34 | I'll guide you through creating an effective AI Action for your use case. Here's a step-by-step approach:
 35 | 
 36 | ## 1. Define your AI Action
 37 | 
 38 | Let's start by clearly defining what your AI Action will do:
 39 | 
 40 | - **Name**: Choose a descriptive name that reflects the action's purpose
 41 | - **Description**: Write a clear description of what the AI Action does and when to use it
 42 | 
 43 | ## 2. Choose the right model
 44 | 
 45 | ${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:
 46 | - **GPT-4** (gpt-4): Best for complex reasoning and high-quality content generation
 47 | - **Claude 3 Opus** (claude-3-opus): Excellent for nuanced understanding and creative content
 48 | - **Claude 3 Sonnet** (claude-3-5-sonnet): Good balance of quality and performance
 49 | - **GPT-3.5 Turbo** (gpt-3.5-turbo): Fastest option for simpler tasks`}
 50 | 
 51 | **Temperature setting**: 
 52 | - Lower (0.0-0.3): More consistent, predictable outputs
 53 | - Medium (0.4-0.7): Balance of creativity and consistency
 54 | - Higher (0.8-1.0): More creative, varied outputs
 55 | 
 56 | ## 3. Design your template
 57 | 
 58 | The template is the prompt that will be sent to the AI model, with placeholders for variables. Here are some best practices:
 59 | 
 60 | - Start with clear instructions about the task
 61 | - Define the desired tone, style, and format
 62 | - Include context about how the output will be used
 63 | - Use variable placeholders with double curly braces: {{variable_name}}
 64 | 
 65 | Example template structure:
 66 | 
 67 | \`\`\`
 68 | You are helping create content for a Contentful entry.
 69 | 
 70 | TASK: [Clear description of what to generate]
 71 | 
 72 | CONTEXT: The content will be used for {{purpose}}.
 73 | 
 74 | TONE: {{tone}}
 75 | 
 76 | BASED ON THIS INFORMATION: {{input_content}}
 77 | 
 78 | GENERATE: [Specific output instructions]
 79 | \`\`\`
 80 | 
 81 | ## 4. Define your variables
 82 | 
 83 | For each placeholder in your template, you'll need to define a variable:
 84 | 
 85 | ### Common variable types:
 86 | 
 87 | 1. **StandardInput**: For primary text input
 88 | 2. **Text**: For simple text fields
 89 | 3. **FreeFormInput**: For custom text input
 90 | 4. **StringOptionsList**: For selecting from predefined options
 91 | 5. **Reference**: For linking to other Contentful entries
 92 | 6. **MediaReference**: For linking to assets (images, videos, etc.)
 93 | 7. **Locale**: For specifying language/region
 94 | 
 95 | For each variable, define:
 96 | - ID: Internal identifier
 97 | - Name: User-friendly display name
 98 | - Description: Clear explanation of what the variable is for
 99 | - Type: One of the types above
100 | - Configuration: Type-specific settings (e.g., option values for StringOptionsList)
101 | 
102 | ## 5. Implementation steps
103 | 
104 | To create this AI Action using the MCP tools:
105 | 
106 | 1. Use the **create_ai_action** tool with these parameters:
107 |    - spaceId, environmentId
108 |    - name, description
109 |    - instruction (containing template and variables)
110 |    - configuration (model type and temperature)
111 | 
112 | 2. Test your AI Action:
113 |    - Add test cases with sample values
114 |    - Verify outputs match expectations
115 | 
116 | 3. Publish your AI Action:
117 |    - Use the **publish_ai_action** tool to make it available to content editors
118 | 
119 | Would you like me to help you structure a specific template and variables for your use case?`,
120 |         },
121 |       },
122 |     ],
123 |   };
124 | }
125 | 
126 | /**
127 |  * Handler for AI Actions variables prompt
128 |  * @param args Optional arguments for the prompt
129 |  * @returns Prompt result with messages
130 |  */
131 | export function handleAiActionsVariables(args?: Record<string, string>): GetPromptResult {
132 |   return {
133 |     messages: [
134 |       {
135 |         role: "assistant",
136 |         content: {
137 |           type: "text",
138 |           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.",
139 |         },
140 |       },
141 |       {
142 |         role: "user",
143 |         content: {
144 |           type: "text",
145 |           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.`,
146 |         },
147 |       },
148 |       {
149 |         role: "assistant",
150 |         content: {
151 |           type: "text",
152 |           text: `# AI Action Variables${args?.variableType ? `: ${args?.variableType} Type` : " Overview"}
153 | 
154 | ${generateVariableTypeContent(args?.variableType)}`,
155 |         },
156 |       },
157 |     ],
158 |   };
159 | }
160 | 
161 | /**
162 |  * Export all AI Actions related handlers
163 |  */
164 | export const aiActionsHandlers = {
165 |   "ai-actions-overview": () => handleAiActionsOverview(),
166 |   "ai-actions-create": (args?: Record<string, string>) => handleAiActionsCreate(args),
167 |   "ai-actions-variables": (args?: Record<string, string>) => handleAiActionsVariables(args),
168 |   "ai-actions-invoke": (args?: Record<string, string>) => handleAiActionsInvoke(args?.actionId, args?.details),
169 | };
```

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

```typescript
  1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"
  2 | import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"
  3 | import { randomUUID } from "crypto"
  4 | import type { Request, Response } from "express"
  5 | 
  6 | /**
  7 |  * Interface for the SSE transport session
  8 |  */
  9 | interface SSESession {
 10 |   id: string
 11 |   response: Response
 12 |   server: Server
 13 |   isClosed: boolean
 14 |   lastEventId?: string
 15 |   heartbeatInterval?: NodeJS.Timeout
 16 | }
 17 | 
 18 | /**
 19 |  * Custom Transport class for SSE
 20 |  */
 21 | class SSEServerTransport {
 22 |   private session: SSESession
 23 | 
 24 |   // Callbacks for the transport
 25 |   onclose?: () => void
 26 |   onerror?: (error: Error) => void
 27 |   onmessage?: (message: JSONRPCMessage) => void
 28 | 
 29 |   constructor(session: SSESession) {
 30 |     this.session = session
 31 |   }
 32 | 
 33 |   async start(): Promise<void> {
 34 |     // Auto-heartbeat every 30 seconds to keep connection alive
 35 |     this.session.heartbeatInterval = setInterval(() => {
 36 |       if (!this.session.isClosed) {
 37 |         try {
 38 |           this.session.response.write(":heartbeat\n\n")
 39 |         } catch (error) {
 40 |           // Connection may be closed, clean up
 41 |           this.clearHeartbeat()
 42 |           SSETransport.closeSession(this.session.id)
 43 |         }
 44 |       } else {
 45 |         this.clearHeartbeat()
 46 |       }
 47 |     }, 30000)
 48 | 
 49 |     // Handle client disconnection
 50 |     this.session.response.on("close", () => {
 51 |       this.clearHeartbeat()
 52 |       SSETransport.closeSession(this.session.id)
 53 |     })
 54 | 
 55 |     // Send initial connection established event
 56 |     this.session.response.write(`event: connected\n`)
 57 |     this.session.response.write(`data: ${JSON.stringify({ sessionId: this.session.id })}\n\n`)
 58 |   }
 59 | 
 60 |   async send(message: JSONRPCMessage): Promise<void> {
 61 |     // Send message to client
 62 |     if (!this.session.isClosed) {
 63 |       try {
 64 |         const data = JSON.stringify(message)
 65 |         this.session.response.write(`id: ${message.id || "notification"}\n`)
 66 |         this.session.response.write(`data: ${data}\n\n`)
 67 |       } catch (error) {
 68 |         console.error(`Error sending SSE message for session ${this.session.id}:`, error)
 69 |       }
 70 |     }
 71 |   }
 72 | 
 73 |   async close(): Promise<void> {
 74 |     this.clearHeartbeat()
 75 |     SSETransport.closeSession(this.session.id)
 76 |   }
 77 | 
 78 |   private clearHeartbeat(): void {
 79 |     if (this.session.heartbeatInterval) {
 80 |       clearInterval(this.session.heartbeatInterval)
 81 |       this.session.heartbeatInterval = undefined
 82 |     }
 83 |   }
 84 | }
 85 | 
 86 | /**
 87 |  * Class to handle server-sent events (SSE) transport for the MCP server
 88 |  */
 89 | export class SSETransport {
 90 |   // Session store for managing active connections
 91 |   private static sessions: Record<string, SSESession> = {}
 92 | 
 93 |   /**
 94 |    * Handle an incoming SSE connection request
 95 |    *
 96 |    * @param req Express request object
 97 |    * @param res Express response object
 98 |    * @returns Session ID for the established connection
 99 |    */
100 |   public static async handleConnection(req: Request, res: Response): Promise<string> {
101 |     // Generate a unique session ID
102 |     const sessionId = randomUUID()
103 | 
104 |     // Set SSE headers
105 |     res.writeHead(200, {
106 |       "Content-Type": "text/event-stream",
107 |       "Cache-Control": "no-cache",
108 |       "Connection": "keep-alive",
109 |       "X-Accel-Buffering": "no", // For Nginx compatibility
110 |     })
111 | 
112 |     // Create a new server instance for this connection
113 |     const server = new Server(
114 |       {
115 |         name: "contentful-mcp-server",
116 |         version: "1.0.0",
117 |       },
118 |       {
119 |         capabilities: {
120 |           tools: {},
121 |           prompts: {},
122 |         },
123 |       },
124 |     )
125 | 
126 |     // Store the session
127 |     const session: SSESession = {
128 |       id: sessionId,
129 |       response: res,
130 |       server,
131 |       isClosed: false,
132 |       lastEventId: req.headers["last-event-id"] as string | undefined,
133 |     }
134 | 
135 |     this.sessions[sessionId] = session
136 | 
137 |     // Create a transport for this session
138 |     const transport = new SSEServerTransport(session)
139 | 
140 |     // Connect the transport to the server
141 |     await server.connect(transport)
142 | 
143 |     // Return the session ID
144 |     return sessionId
145 |   }
146 | 
147 |   /**
148 |    * Handle an incoming message for a session
149 |    *
150 |    * @param req Express request object
151 |    * @param res Express response object
152 |    * @param sessionId Session ID for the connection
153 |    * @param message JSON-RPC message
154 |    */
155 |   public static async handleMessage(
156 |     req: Request,
157 |     res: Response,
158 |     sessionId: string,
159 |     message: JSONRPCMessage
160 |   ): Promise<void> {
161 |     const session = this.sessions[sessionId]
162 | 
163 |     if (!session || session.isClosed) {
164 |       res.status(404).json({
165 |         jsonrpc: "2.0",
166 |         error: {
167 |           code: -32000,
168 |           message: "Session not found or closed",
169 |         },
170 |         id: message.id || null,
171 |       })
172 |       return
173 |     }
174 | 
175 |     try {
176 |       // Get the transport from the server
177 |       // @ts-expect-error - Accessing transport property
178 |       const transport = session.server.transport as SSEServerTransport
179 | 
180 |       // Pass the message to the transport's onmessage handler
181 |       if (transport && transport.onmessage) {
182 |         transport.onmessage(message)
183 |       }
184 | 
185 |       // Send a success response
186 |       res.status(200).json({
187 |         jsonrpc: "2.0",
188 |         result: { success: true },
189 |         id: message.id || null,
190 |       })
191 |     } catch (error) {
192 |       console.error(`Error handling message for session ${sessionId}:`, error)
193 |       res.status(500).json({
194 |         jsonrpc: "2.0",
195 |         error: {
196 |           code: -32603,
197 |           message: `Error processing message: ${error instanceof Error ? error.message : String(error)}`,
198 |         },
199 |         id: message.id || null,
200 |       })
201 |     }
202 |   }
203 | 
204 |   /**
205 |    * Close a session
206 |    *
207 |    * @param sessionId Session ID to close
208 |    */
209 |   public static closeSession(sessionId: string): void {
210 |     const session = this.sessions[sessionId]
211 | 
212 |     if (session && !session.isClosed) {
213 |       session.isClosed = true
214 | 
215 |       try {
216 |         // Clear heartbeat interval if it exists
217 |         if (session.heartbeatInterval) {
218 |           clearInterval(session.heartbeatInterval)
219 |           session.heartbeatInterval = undefined
220 |         }
221 | 
222 |         // End the response
223 |         session.response.end()
224 |       } catch (error) {
225 |         console.error(`Error closing session ${sessionId}:`, error)
226 |       } finally {
227 |         // Delete the session
228 |         delete this.sessions[sessionId]
229 |       }
230 |     }
231 |   }
232 | 
233 |   /**
234 |    * Get a session by ID
235 |    *
236 |    * @param sessionId Session ID
237 |    * @returns Session object or undefined if not found
238 |    */
239 |   public static getSession(sessionId: string): SSESession | undefined {
240 |     return this.sessions[sessionId]
241 |   }
242 | 
243 |   /**
244 |    * Get all active sessions
245 |    *
246 |    * @returns Array of session objects
247 |    */
248 |   public static getAllSessions(): SSESession[] {
249 |     return Object.values(this.sessions)
250 |   }
251 | }
```

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

```typescript
  1 | /* eslint-disable @typescript-eslint/no-explicit-any */
  2 | import { getContentfulClient } from "../config/client.js"
  3 | 
  4 | export const commentHandlers = {
  5 |   getComments: async (args: {
  6 |     spaceId: string
  7 |     environmentId: string
  8 |     entryId: string
  9 |     bodyFormat?: "plain-text" | "rich-text"
 10 |     status?: "active" | "resolved" | "all"
 11 |     limit?: number
 12 |     skip?: number
 13 |   }) => {
 14 |     const spaceId =
 15 |       process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
 16 |         ? process.env.SPACE_ID
 17 |         : args.spaceId
 18 |     const environmentId =
 19 |       process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
 20 |         ? process.env.ENVIRONMENT_ID
 21 |         : args.environmentId
 22 |     const { entryId, bodyFormat = "plain-text", status = "active", limit = 10, skip = 0 } = args
 23 | 
 24 |     const baseParams = {
 25 |       spaceId,
 26 |       environmentId,
 27 |       entryId,
 28 |     }
 29 | 
 30 |     const contentfulClient = await getContentfulClient()
 31 | 
 32 |     // Build query based on status filter
 33 |     const query: { status?: "active" | "resolved" } = {}
 34 |     if (status !== "all") {
 35 |       query.status = status
 36 |     }
 37 | 
 38 |     // Handle different bodyFormat types separately due to TypeScript overloads
 39 |     const comments =
 40 |       bodyFormat === "rich-text"
 41 |         ? await contentfulClient.comment.getMany({
 42 |             ...baseParams,
 43 |             bodyFormat: "rich-text" as const,
 44 |             query,
 45 |           })
 46 |         : await contentfulClient.comment.getMany({
 47 |             ...baseParams,
 48 |             bodyFormat: "plain-text" as const,
 49 |             query,
 50 |           })
 51 | 
 52 |     // Apply manual pagination since Contentful Comments API doesn't support it
 53 |     const startIndex = skip
 54 |     const endIndex = skip + limit
 55 |     const paginatedItems = comments.items.slice(startIndex, endIndex)
 56 | 
 57 |     const paginatedResult = {
 58 |       items: paginatedItems,
 59 |       total: comments.total,
 60 |       showing: paginatedItems.length,
 61 |       remaining: Math.max(0, comments.total - endIndex),
 62 |       skip: endIndex < comments.total ? endIndex : undefined,
 63 |       message:
 64 |         endIndex < comments.total
 65 |           ? "To see more comments, use skip parameter with the provided skip value."
 66 |           : undefined,
 67 |     }
 68 | 
 69 |     return {
 70 |       content: [
 71 |         {
 72 |           type: "text",
 73 |           text: JSON.stringify(paginatedResult, null, 2),
 74 |         },
 75 |       ],
 76 |     }
 77 |   },
 78 | 
 79 |   createComment: async (args: {
 80 |     spaceId: string
 81 |     environmentId: string
 82 |     entryId: string
 83 |     body: string
 84 |     status?: "active"
 85 |     parent?: string
 86 |   }) => {
 87 |     const spaceId =
 88 |       process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
 89 |         ? process.env.SPACE_ID
 90 |         : args.spaceId
 91 |     const environmentId =
 92 |       process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
 93 |         ? process.env.ENVIRONMENT_ID
 94 |         : args.environmentId
 95 |     const { entryId, body, parent } = args
 96 | 
 97 |     const baseParams = {
 98 |       spaceId,
 99 |       environmentId,
100 |       entryId,
101 |       // Add parentCommentId to baseParams when parent is provided
102 |       ...(parent && { parentCommentId: parent }),
103 |     }
104 | 
105 |     const contentfulClient = await getContentfulClient()
106 | 
107 |     // Simple comment data object (no parent in body)
108 |     const commentData = {
109 |       body,
110 |       status: "active" as const,
111 |     }
112 | 
113 |     const comment = await contentfulClient.comment.create(baseParams, commentData)
114 | 
115 |     return {
116 |       content: [
117 |         {
118 |           type: "text",
119 |           text: JSON.stringify(comment, null, 2),
120 |         },
121 |       ],
122 |     }
123 |   },
124 | 
125 |   getSingleComment: async (args: {
126 |     spaceId: string
127 |     environmentId: string
128 |     entryId: string
129 |     commentId: string
130 |     bodyFormat?: "plain-text" | "rich-text"
131 |   }) => {
132 |     const spaceId =
133 |       process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
134 |         ? process.env.SPACE_ID
135 |         : args.spaceId
136 |     const environmentId =
137 |       process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
138 |         ? process.env.ENVIRONMENT_ID
139 |         : args.environmentId
140 |     const { entryId, commentId, bodyFormat = "plain-text" } = args
141 | 
142 |     const baseParams = {
143 |       spaceId,
144 |       environmentId,
145 |       entryId,
146 |       commentId,
147 |     }
148 | 
149 |     const contentfulClient = await getContentfulClient()
150 | 
151 |     // Handle different bodyFormat types separately due to TypeScript overloads
152 |     const comment =
153 |       bodyFormat === "rich-text"
154 |         ? await contentfulClient.comment.get({
155 |             ...baseParams,
156 |             bodyFormat: "rich-text" as const,
157 |           })
158 |         : await contentfulClient.comment.get({
159 |             ...baseParams,
160 |             bodyFormat: "plain-text" as const,
161 |           })
162 | 
163 |     return {
164 |       content: [
165 |         {
166 |           type: "text",
167 |           text: JSON.stringify(comment, null, 2),
168 |         },
169 |       ],
170 |     }
171 |   },
172 | 
173 |   deleteComment: async (args: {
174 |     spaceId: string
175 |     environmentId: string
176 |     entryId: string
177 |     commentId: string
178 |   }) => {
179 |     const spaceId =
180 |       process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
181 |         ? process.env.SPACE_ID
182 |         : args.spaceId
183 |     const environmentId =
184 |       process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
185 |         ? process.env.ENVIRONMENT_ID
186 |         : args.environmentId
187 |     const { entryId, commentId } = args
188 | 
189 |     const baseParams = {
190 |       spaceId,
191 |       environmentId,
192 |       entryId,
193 |       commentId,
194 |     }
195 | 
196 |     const contentfulClient = await getContentfulClient()
197 | 
198 |     // First get the comment to obtain its version
199 |     const comment = await contentfulClient.comment.get({
200 |       ...baseParams,
201 |       bodyFormat: "plain-text" as const,
202 |     })
203 | 
204 |     // Now delete with the version
205 |     await contentfulClient.comment.delete({
206 |       ...baseParams,
207 |       version: comment.sys.version,
208 |     })
209 | 
210 |     return {
211 |       content: [
212 |         {
213 |           type: "text",
214 |           text: JSON.stringify(
215 |             {
216 |               success: true,
217 |               message: `Successfully deleted comment ${commentId} from entry ${entryId}`,
218 |             },
219 |             null,
220 |             2,
221 |           ),
222 |         },
223 |       ],
224 |     }
225 |   },
226 | 
227 |   updateComment: async (args: {
228 |     spaceId: string
229 |     environmentId: string
230 |     entryId: string
231 |     commentId: string
232 |     body?: string
233 |     status?: "active" | "resolved"
234 |     bodyFormat?: "plain-text" | "rich-text"
235 |   }) => {
236 |     const spaceId =
237 |       process.env.SPACE_ID && process.env.SPACE_ID !== "undefined"
238 |         ? process.env.SPACE_ID
239 |         : args.spaceId
240 |     const environmentId =
241 |       process.env.ENVIRONMENT_ID && process.env.ENVIRONMENT_ID !== "undefined"
242 |         ? process.env.ENVIRONMENT_ID
243 |         : args.environmentId
244 |     const { entryId, commentId, body, status, bodyFormat = "plain-text" } = args
245 | 
246 |     const baseParams = {
247 |       spaceId,
248 |       environmentId,
249 |       entryId,
250 |       commentId,
251 |     }
252 | 
253 |     // Build update data object with only provided fields
254 |     const updateData: { body?: string; status?: "active" | "resolved" } = {}
255 |     if (body !== undefined) updateData.body = body
256 |     if (status !== undefined) updateData.status = status
257 | 
258 |     const contentfulClient = await getContentfulClient()
259 | 
260 |     // First get the comment to obtain its version
261 |     const existingComment =
262 |       bodyFormat === "rich-text"
263 |         ? await contentfulClient.comment.get({
264 |             ...baseParams,
265 |             bodyFormat: "rich-text" as const,
266 |           })
267 |         : await contentfulClient.comment.get({
268 |             ...baseParams,
269 |             bodyFormat: "plain-text" as const,
270 |           })
271 | 
272 |     // Update with the version
273 |     const comment = await contentfulClient.comment.update(baseParams, {
274 |       ...updateData,
275 |       version: existingComment.sys.version,
276 |     })
277 | 
278 |     return {
279 |       content: [
280 |         {
281 |           type: "text",
282 |           text: JSON.stringify(comment, null, 2),
283 |         },
284 |       ],
285 |     }
286 |   },
287 | }
288 | 
```

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

```typescript
  1 | import { expect, vi, describe, it, beforeEach } from "vitest"
  2 | import { contentTypeHandlers } from "../../src/handlers/content-type-handlers.js"
  3 | 
  4 | // Define constants
  5 | const TEST_CONTENT_TYPE_ID = "test-content-type-id"
  6 | const TEST_SPACE_ID = "test-space-id"
  7 | const TEST_ENV_ID = "master"
  8 | 
  9 | // Mock the Contentful client for testing the merge logic
 10 | vi.mock("../../src/config/client.js", () => {
 11 |   return {
 12 |     getContentfulClient: vi.fn().mockImplementation(() => {
 13 |       // Create mock content type inside the function implementation
 14 |       const mockContentType = {
 15 |         sys: { id: TEST_CONTENT_TYPE_ID, version: 1 },
 16 |         name: "Original Content Type",
 17 |         description: "Original description",
 18 |         displayField: "title",
 19 |         fields: [
 20 |           {
 21 |             id: "title",
 22 |             name: "Title",
 23 |             type: "Text",
 24 |             required: true,
 25 |             validations: [{ size: { max: 100 } }]
 26 |           },
 27 |           {
 28 |             id: "description",
 29 |             name: "Description",
 30 |             type: "Text",
 31 |             required: false
 32 |           },
 33 |           {
 34 |             id: "image",
 35 |             name: "Image",
 36 |             type: "Link",
 37 |             linkType: "Asset",
 38 |             required: false
 39 |           },
 40 |           {
 41 |             id: "tags",
 42 |             name: "Tags",
 43 |             type: "Array",
 44 |             items: {
 45 |               type: "Symbol"
 46 |             }
 47 |           }
 48 |         ]
 49 |       }
 50 | 
 51 |       return {
 52 |         contentType: {
 53 |           get: vi.fn().mockResolvedValue(mockContentType),
 54 |           update: vi.fn().mockImplementation((params, contentTypeProps) => {
 55 |             // Return a merged content type that simulates the updated fields
 56 |             return Promise.resolve({
 57 |               sys: { id: params.contentTypeId, version: 2 },
 58 |               name: contentTypeProps.name,
 59 |               description: contentTypeProps.description,
 60 |               displayField: contentTypeProps.displayField,
 61 |               fields: contentTypeProps.fields
 62 |             })
 63 |           })
 64 |         }
 65 |       }
 66 |     })
 67 |   }
 68 | })
 69 | 
 70 | describe("Content Type Handler Merge Logic", () => {
 71 |   beforeEach(() => {
 72 |     vi.clearAllMocks()
 73 |   })
 74 | 
 75 |   it("should use existing name when name is not provided", async () => {
 76 |     // Setup - update without providing name
 77 |     const updateData = {
 78 |       spaceId: TEST_SPACE_ID,
 79 |       environmentId: TEST_ENV_ID,
 80 |       contentTypeId: TEST_CONTENT_TYPE_ID,
 81 |       fields: [
 82 |         {
 83 |           id: "title",
 84 |           name: "Title",
 85 |           type: "Text",
 86 |           required: true,
 87 |           validations: [{ size: { max: 100 } }]
 88 |         },
 89 |         {
 90 |           id: "description",
 91 |           name: "Description",
 92 |           type: "Text",
 93 |           required: false
 94 |         },
 95 |         {
 96 |           id: "image",
 97 |           name: "Image",
 98 |           type: "Link",
 99 |           linkType: "Asset",
100 |           required: false
101 |         },
102 |         {
103 |           id: "tags",
104 |           name: "Tags",
105 |           type: "Array",
106 |           items: {
107 |             type: "Symbol"
108 |           }
109 |         }
110 |       ],
111 |       description: "Updated description"
112 |     }
113 | 
114 |     // Execute
115 |     const result = await contentTypeHandlers.updateContentType(updateData)
116 |     
117 |     // Parse the result
118 |     const updatedContentType = JSON.parse(result.content[0].text)
119 |     
120 |     // Assert - should keep the original name but update description
121 |     expect(updatedContentType.name).toEqual("Original Content Type")
122 |     expect(updatedContentType.description).toEqual("Updated description")
123 |   })
124 | 
125 |   it("should preserve field metadata when updating fields", async () => {
126 |     // Setup - update with simplified field definition that's missing metadata
127 |     const updateData = {
128 |       spaceId: TEST_SPACE_ID,
129 |       environmentId: TEST_ENV_ID,
130 |       contentTypeId: TEST_CONTENT_TYPE_ID,
131 |       fields: [
132 |         {
133 |           id: "title",
134 |           name: "New Title Name",
135 |           type: "Text"
136 |           // Intentionally missing required, validations, etc.
137 |         },
138 |         {
139 |           id: "image",
140 |           name: "Updated Image",
141 |           type: "Link"
142 |           // Missing linkType
143 |         },
144 |         {
145 |           id: "tags",
146 |           name: "Updated Tags",
147 |           type: "Array"
148 |           // Missing items definition
149 |         }
150 |       ]
151 |     }
152 | 
153 |     // Execute
154 |     const result = await contentTypeHandlers.updateContentType(updateData)
155 |     
156 |     // Parse the result
157 |     const updatedContentType = JSON.parse(result.content[0].text)
158 |     
159 |     // Assert - fields should be updated but metadata should be preserved
160 |     const titleField = updatedContentType.fields.find(f => f.id === "title")
161 |     expect(titleField.name).toEqual("New Title Name") // Updated
162 |     expect(titleField.required).toEqual(true) // Preserved from original
163 |     expect(titleField.validations).toEqual([{ size: { max: 100 } }]) // Preserved from original
164 |     
165 |     const imageField = updatedContentType.fields.find(f => f.id === "image")
166 |     expect(imageField.name).toEqual("Updated Image") // Updated
167 |     expect(imageField.linkType).toEqual("Asset") // Preserved from original
168 |     
169 |     const tagsField = updatedContentType.fields.find(f => f.id === "tags")
170 |     expect(tagsField.name).toEqual("Updated Tags") // Updated
171 |     expect(tagsField.items).toEqual({ type: "Symbol" }) // Preserved from original
172 |   })
173 | 
174 |   it("should handle adding new fields", async () => {
175 |     // Define the original and a new field
176 |     const originalFields = [
177 |       {
178 |         id: "title",
179 |         name: "Title",
180 |         type: "Text",
181 |         required: true,
182 |         validations: [{ size: { max: 100 } }]
183 |       },
184 |       {
185 |         id: "description",
186 |         name: "Description",
187 |         type: "Text",
188 |         required: false
189 |       },
190 |       {
191 |         id: "image",
192 |         name: "Image",
193 |         type: "Link",
194 |         linkType: "Asset",
195 |         required: false
196 |       },
197 |       {
198 |         id: "tags",
199 |         name: "Tags",
200 |         type: "Array",
201 |         items: {
202 |           type: "Symbol"
203 |         }
204 |       }
205 |     ]
206 |     
207 |     const newField = {
208 |       id: "newField",
209 |       name: "New Field",
210 |       type: "Boolean",
211 |       required: false
212 |     }
213 |     
214 |     // Setup - add a new field
215 |     const updateData = {
216 |       spaceId: TEST_SPACE_ID,
217 |       environmentId: TEST_ENV_ID,
218 |       contentTypeId: TEST_CONTENT_TYPE_ID,
219 |       fields: [...originalFields, newField]
220 |     }
221 | 
222 |     // Execute
223 |     const result = await contentTypeHandlers.updateContentType(updateData)
224 |     
225 |     // Parse the result
226 |     const updatedContentType = JSON.parse(result.content[0].text)
227 |     
228 |     // Assert - should have all original fields plus the new one
229 |     expect(updatedContentType.fields.length).toEqual(5) // 4 original + 1 new
230 |     const addedField = updatedContentType.fields.find(f => f.id === "newField")
231 |     expect(addedField).toEqual({
232 |       id: "newField",
233 |       name: "New Field",
234 |       type: "Boolean",
235 |       required: false
236 |     })
237 |   })
238 | 
239 |   it("should handle when no fields are provided", async () => {
240 |     // Setup - update without providing fields, should use existing fields
241 |     const updateData = {
242 |       spaceId: TEST_SPACE_ID,
243 |       environmentId: TEST_ENV_ID,
244 |       contentTypeId: TEST_CONTENT_TYPE_ID,
245 |       name: "Updated Content Type Name"
246 |     }
247 | 
248 |     // Execute
249 |     const result = await contentTypeHandlers.updateContentType(updateData)
250 |     
251 |     // Parse the result
252 |     const updatedContentType = JSON.parse(result.content[0].text)
253 |     
254 |     // Assert - should use existing fields but updated name
255 |     expect(updatedContentType.name).toEqual("Updated Content Type Name")
256 |     expect(updatedContentType.fields.length).toEqual(4) // All 4 original fields should be preserved
257 |     
258 |     // Check that all original fields are preserved
259 |     const titleField = updatedContentType.fields.find(f => f.id === "title")
260 |     expect(titleField).toBeDefined()
261 |     expect(titleField.name).toEqual("Title")
262 |     
263 |     const descriptionField = updatedContentType.fields.find(f => f.id === "description")
264 |     expect(descriptionField).toBeDefined()
265 |     expect(descriptionField.name).toEqual("Description")
266 |   })
267 | })
```
Page 1/3FirstPrevNextLast