An MCP server exposing barcode/GTIN tools, deployed as an AWS Lambda function behind an API Gateway HTTP API, provisioned with Terraform.
| Tool | Description |
|---|---|
validate_gtin |
Validates a GTIN-8/12/13/14 (EAN/UPC) number by recomputing its GS1 check digit. |
generate_gtin |
Generates a random, check-digit-valid GTIN of a given length, optionally with a fixed prefix. |
generate_barcode_image |
Renders a barcode PNG for a code + symbology (EAN13, EAN8, UPCA, UPCE, Code128, Code39, ITF14, QR Code, Data Matrix, PDF417, Aztec). |
decode_barcode_image |
Decodes a barcode (1D or 2D) from a base64-encoded image. |
Barcode encoding/decoding is powered by zxing-wasm, a WebAssembly build of the real zxing-cpp library — this was a deliberate choice over pure-JS ports, which turned out to have an unreliable EAN/UPC reader during testing.
- Runtime: Node.js 22, TypeScript, bundled to a single CommonJS file with esbuild.
- MCP transport:
@modelcontextprotocol/sdk'sWebStandardStreamableHTTPServerTransport, used directly with the Fetch APIRequest/Responseobjects — no Express, no Lambda web-adapter layer.src/lambda.tsadapts API Gateway'sAPIGatewayProxyEventV2to/from those Fetch types. The transport runs stateless (no session ID, plain JSON responses instead of SSE) since API Gateway + Lambda is a buffered request/response model, not a place to hold long-lived SSE streams open. - Infra: API Gateway HTTP API → Lambda (
ANY /mcp), Terraform-managed, arm64.
MCP client -> API Gateway (HTTP API) -> Lambda (index.handler) -> McpServer + tools
src/
mcpServer.ts # tool registration (shared by lambda + local dev)
lambda.ts # Lambda handler: API Gateway <-> Fetch Request/Response
local.ts # plain node:http server for local testing
tools/
gtin.ts # GTIN check-digit math (validate/generate)
generateBarcode.ts # zxing-wasm writer
decodeBarcode.ts # zxing-wasm reader
zxingModule.ts # loads the zxing WASM binary for Node
terraform/ # API Gateway + Lambda + IAM + CloudWatch
esbuild.config.mjs # bundles src/lambda.ts -> dist/index.js (+ copies the .wasm)
- Node.js 22+
- Terraform >= 1.5
- An AWS account/credentials configured (e.g. via
aws configureor environment variables) with permission to create Lambda, API Gateway, IAM and CloudWatch Logs resources.
npm install
npm run dev
# -> mcp-zebra listening locally at http://localhost:3000/mcpTalk to it with any MCP client that supports Streamable HTTP, pointed at http://localhost:3000/mcp.
You can also poke it directly with curl, e.g. to list tools:
curl -s http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| jq '.result.tools[] | {name, description}'npm install
npm run tf:init # terraform init (first time only)
npm run tf:apply # builds the Lambda bundle, zips it, then terraform applynpm run tf:apply runs npm run package first (esbuild bundle + copy the zxing WASM binary +
zip into build/lambda.zip), so Terraform always deploys the code you just built. Terraform will
print mcp_endpoint_url when it's done — that's the URL to point your MCP client at.
To tear everything down:
cd terraform && terraform destroy| Script | What it does |
|---|---|
npm run dev |
Local dev server with hot reload (tsx watch src/local.ts). |
npm run typecheck |
tsc --noEmit |
npm run build |
esbuild bundle to dist/index.js + copy zxing_full.wasm next to it. |
npm run package |
build + zip into build/lambda.zip. |
npm run tf:plan |
package + terraform plan. |
npm run tf:apply |
package + terraform apply. |
All optional (see terraform/variables.tf for defaults): aws_region (default us-east-1),
project_name (default mcp-zebra), stage_name (default prod), lambda_memory_size,
lambda_timeout, log_retention_days, tags.
terraform apply -var="aws_region=eu-west-1" -var="stage_name=dev"- CORS is wide open (
*) and there's no authentication in front of the endpoint — fine for a demo, not for anything handling sensitive data. Put an authorizer (e.g. an API Gateway JWT authorizer) in front of it before using this for anything real. - A fresh
McpServer/transport pair is created per request rather than reused across warm Lambda invocations. This matches the SDK's documented stateless-HTTP pattern and sidesteps known bugs around reusing a stateless transport across requests; the cost (re-registering four tools) is negligible next to Lambda's own cold-start overhead.
The Lambda function targets nodejs22.x (terraform/lambda.tf), currently the newest
AWS Lambda-managed Node.js runtime. AWS Lambda's minimum supported runtime is nodejs20.x
(nodejs18.x and older have reached end-of-support and can no longer be created or updated).
Track supported/deprecated runtimes at the
AWS Lambda runtimes docs —
each runtime is deprecated roughly in line with the corresponding Node.js version's own
upstream end-of-life, after which AWS stops
patching it (functions keep running, but on an unmaintained runtime).