Development 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.

This section covers the handler project structure, local development workflow, and testing setup for Adobe LLM Apps. For the handler contract and example code, see Write the Action Handler.

Project structure

Your linked repository follows this layout:

your-llm-app/
├── entry.js                   # Webpack entry — do not modify
├── actions/                   # One folder per action
│   ├── search-products/
│   │   └── index.js           # Handler (async function)
│   ├── get-product-details/
│   │   └── index.js
│   └── echo/
│       └── index.js
├── test/
│   ├── actions/
│   │   └── search-products.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 — local copy of UI 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. Download it from the Actions page in the UI for local development. For deployments, the pipeline writes it automatically from the API.
  • 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.

Download actions.json

For the local server to know about your action metadata (name, description, input schema), download actions.json from the Actions page in the LLM Apps UI and place it at the repository root. Without it, the server discovers your handlers but registers them with minimal metadata.

You can also copy actions.example.json to actions.json as a starting point.

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 search-products 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":"search-products","arguments":{"category":"bagged-coffee"}}}'

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/search-products.test.js
const handler = require('../../actions/search-products/index.js')

test('returns all products when no filter is given', async () => {
  const result = await handler({})
  expect(result.content[0].text).toContain('product')
  expect(result.structuredContent.products.length).toBeGreaterThan(0)
})

test('filters by category', async () => {
  const result = await handler({ category: 'bagged-coffee' })
  expect(result.structuredContent.products.every(
    (p) => p.category === 'bagged-coffee'
  )).toBe(true)
})

test('filters by query', async () => {
  const result = await handler({ query: 'dark-roast' })
  expect(result.structuredContent.products.length).toBeGreaterThan(0)
})

test('returns empty result for unknown category', async () => {
  const result = await handler({ category: 'nonexistent' })
  expect(result.structuredContent.products).toHaveLength(0)
})

Run tests with:

npm test                                      # all tests
npx jest test/actions/search-products        # one action only

Deployment

You do not build or deploy manually. For a full walkthrough of the deployment pipeline, see Deploy Your App.

Your day-to-day workflow is:

Step
Action
1. Write or edit handler
actions/<name>/index.js
2. Download metadata
Actions page → Download actions.json
3. Test locally
npm run dev:local
4. Push code
git push
5. Deploy
App Detail page → Deploy
recommendation-more-help
llm-apps-help-main-toc