Local Handler Development and Testing development

IMPORTANT
Adobe LLM Apps is currently in Beta.
Features, workflows, and UI shown here do not necessarily represent the final state of the product. To join the Beta, send an email to llm-apps-beta@adobe.com.

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.js is the webpack entry point. At build time it discovers every actions/*/index.js file and bundles them into a single dist/index.js. Do not modify.
  • actions.json is gitignored. The deployment pipeline writes it automatically from the action metadata in LLM Apps.
  • Tests live under test/actions/, not inside actions/. Webpack bundles everything under actions/ 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.

recommendation-more-help
llm-apps-help-main-toc