Local Handler Development and Testing development
Use this reference while developing handlers locally. For the handler result contract, see Customize a generated handler.
Requirements
- Node.js 24 or later.
- npm.
- A local clone of the linked handler repository.
Project structure
Your linked repository follows this layout:
your-llm-app/
├── entry.js # Webpack entry — do not modify
├── actions/ # One folder per action
│ └── echo/
│ └── index.js # Example handler
├── test/
│ ├── actions/
│ │ └── echo.test.js
│ ├── fixtures/
│ │ └── actions.json
│ ├── html-transform.js
│ ├── jest.setup.js
│ └── server.test.js
├── server/
│ └── local.js # Local dev server (port 9080)
├── actions.json # Gitignored — optional local metadata
├── app.config.yaml # Adobe I/O Runtime config
├── webpack.config.js
└── package.json
Key points:
entry.jsis the webpack entry point. At build time it discovers everyactions/*/index.jsfile and bundles them into a singledist/index.js. Do not modify.actions.jsonis gitignored. The deployment pipeline writes it automatically from the action metadata in LLM Apps.- Tests live under
test/actions/, not insideactions/. Webpack bundles everything underactions/into the deployed artifact — co-locating tests would ship them to Adobe I/O Runtime.
Local development
You can develop and test handlers locally without Adobe credentials:
npm install
npm run dev:local
This builds the project with webpack and starts a plain Node.js HTTP server on http://localhost:9080. The server auto-discovers your handler files under actions/ and registers them as MCP tools.
Local metadata behavior
The current UI does not provide an actions.json download. You can run the local server without this file; it discovers handlers under actions/ and registers them with minimal metadata.
Without actions.json, local action arguments are not validated against the UI input schema. Unit and integration tests use test/fixtures/actions.json for representative metadata.
Test with curl
# List all registered tools
curl -sX POST "http://localhost:9080" \
-H 'content-type: application/json' \
-H 'accept: application/json;q=1.0, text/event-stream;q=0.5' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# Call the boilerplate echo action
curl -sX POST "http://localhost:9080" \
-H 'content-type: application/json' \
-H 'accept: application/json;q=1.0, text/event-stream;q=0.5' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello"}}}'
Test with MCP Inspector
npx @modelcontextprotocol/inspector
Set Transport Type to streamable-http and URL to http://localhost:9080.
Testing
Handler unit tests live under test/actions/ and mirror the actions/ layout:
// test/actions/echo.test.js
const handler = require('../../actions/echo/index.js')
test('echoes the message', async () => {
const result = await handler({ message: 'hello' })
expect(result.content[0].text).toBe('Echo: hello')
})
test('always returns content parts', async () => {
const result = await handler({})
expect(Array.isArray(result.content)).toBe(true)
})
Run tests with:
npm test # all tests
npx jest test/actions/echo # one action only
After local tests pass, push the changes and follow Deploy changes.