Home / Articles

WeatherSG MCP App: Tools Plus Interactive Map in ChatGPT and Claude

WeatherSG, an MCP App in TypeScript: data contract, Streamable HTTP, Cloud Run deploy and host registration so ChatGPT and Claude show a live weather map.

Publisher: REACTAPP.TOP16 min read3664 words

This article is published in English.

Sources: How to Build and Deploy an MCP App

MCP Apps extend Model Context Protocol beyond tools that return text: they can ship an interactive UI resource hosts like ChatGPT or Claude can present. This walkthrough builds WeatherSG in TypeScript—data contract, weather fetch, ordinary tool, map UI bundled as one HTML resource, Streamable HTTP transport, container deploy to Cloud Run, and registration in both hosts.

What an MCP App is

A server still exposes tools and resources; an App additionally serves a front-end the host embeds after a tool call chooses it. The loop is prompt → tool → UI resource → user interaction → further tool calls as needed.

Why TypeScript here

One language for server and UI bundling keeps the deploy unit small. Strict types help the data contract stay honest between normalisers and the map.

mkdir weathersg
cd weathersg
npm init -y

npm install \
  @modelcontextprotocol/sdk \
  @modelcontextprotocol/ext-apps \
  express leaflet react react-dom zod

npm install --save-dev \
  @types/express @types/leaflet @types/node \
  @types/react @types/react-dom \
  esbuild supertest tsx typescript vitest

Keep secrets in the platform env; never bake API keys into the bundled HTML.

{
  "scripts": {
    "build": "npm run build:web && tsc -p tsconfig.json",
    "build:web": "node scripts/build-web.mjs",
    "dev": "npm run build:web && tsx watch src/index.ts",
    "start": "node dist/src/index.js",
    "test": "vitest run",
    "typecheck": "tsc -p tsconfig.json --noEmit"
  },
  "engines": {
    "node": ">=20"
  }
}

Prefer streamable HTTP for hosts that expect a durable MCP session URL.

weathersg/
  src/
    weather/
      schema.ts
      client.ts
    http-app.ts
    index.ts
    server.ts
  web/
    src/
      main.tsx
      styles.css
  scripts/
    build-web.mjs
  tests/
  Dockerfile
  package.json
  tsconfig.json

Container health checks must hit the MCP endpoint, not only a static page.

import { z } from "zod";

export const weatherLayerSchema = z.enum([
  "all",
  "forecast",
  "rainfall",
]);

const forecastAreaSchema = z.object({
  id: z.string(),
  name: z.string(),
  latitude: z.number(),
  longitude: z.number(),
  condition: z.string(),
  category: z.enum([
    "clear",
    "cloudy",
    "showers",
    "thunderstorms",
    "windy",
    "hazy",
    "unknown",
  ]),
});

const rainfallStationSchema = z.object({
  id: z.string(),
  name: z.string(),
  latitude: z.number(),
  longitude: z.number(),
  rainfallMm: z.number().nonnegative(),
});

export const weatherSnapshotSchema = z.object({
  layer: weatherLayerSchema,
  generatedAt: z.string(),
  forecast: z.object({
    updatedAt: z.string(),
    validFrom: z.string(),
    validTo: z.string(),
    validPeriodText: z.string(),
    areas: z.array(forecastAreaSchema),
  }).nullable(),
  rainfall: z.object({
    observedAt: z.string(),
    readingType: z.string(),
    unit: z.string(),
    stations: z.array(rainfallStationSchema),
  }).nullable(),
  summary: z.object({
    headline: z.string(),
    areasWithRain: z.number().int().nonnegative(),
    reportingRainStations: z.number().int().nonnegative(),
  }),
  warnings: z.array(z.string()),
  sources: z.array(z.object({
    label: z.string(),
    url: z.string().url(),
  })),
});

export type WeatherSnapshot = z.infer<typeof weatherSnapshotSchema>;

Host registration is configuration—retest after every URL or auth change.

async function fetchValidated<T>(
  url: string,
  schema: z.ZodType<T>,
  sourceLabel: string,
): Promise<{ value: T; warning?: string }> {
  const cached = cache.get(url);

  try {
    const response = await fetch(url, {
      headers: { Accept: "application/json" },
      signal: AbortSignal.timeout(8_000),
    });

    if (!response.ok) {
      throw new Error(`Weather service returned HTTP ${response.status}`);
    }
    const value = schema.parse(await response.json());
    cache.set(url, {
      value,
      expiresAt: Date.now() + 60_000,
    });
    return { value };
  } catch (error) {
    if (cached) {
      return {
        value: cached.value as T,
        warning: `${sourceLabel} could not be refreshed`,
      };
    }
    throw error;
  }
}

Treat this as a checkpoint: protocol tests should pass before any chat-host wiring.

const [forecastResult, rainfallResult] = await Promise.allSettled([
  fetchValidated(TWO_HOUR_FORECAST_URL, forecastSchema, "Forecast"),
  fetchValidated(RAINFALL_URL, rainfallSchema, "Rainfall"),
]);

Keep secrets in the platform env; never bake API keys into the bundled HTML.

return weatherSnapshotSchema.parse(snapshot);

Prefer streamable HTTP for hosts that expect a durable MCP session URL.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getSingaporeWeather } from "./weather/client.js";
import {
  weatherLayerSchema,
  weatherSnapshotSchema,
} from "./weather/schema.js";

const server = new McpServer(
  { name: "weathersg", version: "0.1.0" },
  {
    instructions:
      "Use get_singapore_weather for factual weather questions. " +
      "Use show_singapore_weather_map when the user asks to see, " +
      "map, compare, or explore weather across Singapore.",
  },
);

Container health checks must hit the MCP endpoint, not only a static page.

server.registerTool(
  "get_singapore_weather",
  {
    title: "Get Singapore weather",
    description:
      "Get current official two-hour forecasts and five-minute " +
      "rainfall observations across Singapore.",
    inputSchema: {
      layer: weatherLayerSchema.default("all"),
    },
    outputSchema: weatherSnapshotSchema,
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      openWorldHint: false,
    },
  },
  async ({ layer }) => {
    const snapshot = await getSingaporeWeather(layer);
    return {
      structuredContent: snapshot,
      content: [{
        type: "text",
        text: `${snapshot.summary.headline} Snapshot generated at ${snapshot.generatedAt}.`,
      }],
    };
  },
);

Host registration is configuration—retest after every URL or auth change.

import {
  registerAppResource,
  registerAppTool,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";

export const WEATHER_APP_URI =
  "ui://weathersg/singapore-weather-map/v1.html";

Treat this as a checkpoint: protocol tests should pass before any chat-host wiring.

registerAppTool(
  server,
  "show_singapore_weather_map",
  {
    title: "Show Singapore weather map",
    description:
      "Show an interactive map of official forecasts and rainfall " +
      "observations across Singapore.",
    inputSchema: {
      layer: weatherLayerSchema.default("all"),
    },
    outputSchema: weatherSnapshotSchema,
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      openWorldHint: false,
    },
    _meta: {
      ui: { resourceUri: WEATHER_APP_URI },
      "openai/toolInvocation/invoking": "Reading Singapore weather…",
      "openai/toolInvocation/invoked": "Singapore weather map ready",
    },
  },
  async ({ layer }) => {
    const snapshot = await getSingaporeWeather(layer);
    return {
      structuredContent: snapshot,
      content: [{
        type: "text",
        text: snapshot.summary.headline,
      }],
    };
  },
);

Keep secrets in the platform env; never bake API keys into the bundled HTML.

registerAppResource(
  server,
  "Singapore weather map",
  WEATHER_APP_URI,
  { description: "Interactive map of Singapore weather" },
  async () => ({
    contents: [{
      uri: WEATHER_APP_URI,
      mimeType: RESOURCE_MIME_TYPE,
      text: loadWeatherAppHtml(),
      _meta: {
        ui: {
          prefersBorder: true,
          csp: {
            connectDomains: [],
            resourceDomains: ["https://www.onemap.gov.sg"],
          },
        },
      },
    }],
  }),
);

Prefer streamable HTTP for hosts that expect a durable MCP session URL.

import { App } from "@modelcontextprotocol/ext-apps";
import { createRoot } from "react-dom/client";

const app = new App(
  { name: "WeatherSG map", version: "0.1.0" },
  {},
  { autoResize: true },
);

function parseSnapshot(value: unknown): WeatherSnapshot | null {
  if (!value || typeof value !== "object") return null;
  const candidate = value as Partial<WeatherSnapshot>;
  if (!candidate.summary || !Array.isArray(candidate.sources)) return null;
  if (!candidate.forecast && !candidate.rainfall) return null;
  return candidate as WeatherSnapshot;
}

app.ontoolresult = (result) => {
  const snapshot = parseSnapshot(result.structuredContent);
  if (snapshot) deliverSnapshot(snapshot);
};

createRoot(document.getElementById("root")!).render(<WeatherApp />);

void app.connect().catch((error) => {
  console.error("Unable to connect to the host", error);
});

Container health checks must hit the MCP endpoint, not only a static page.

await build({
  entryPoints: ["web/src/main.tsx"],
  bundle: true,
  minify: true,
  format: "esm",
  platform: "browser",
  write: false,
  outdir: "dist/web-bundle",
});

Host registration is configuration—retest after every URL or auth change.

npm run build

Treat this as a checkpoint: protocol tests should pass before any chat-host wiring.

import { createMcpExpressApp } from
  "@modelcontextprotocol/sdk/server/express.js";
import { StreamableHTTPServerTransport } from
  "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createWeatherMcpServer } from "./server.js";

const app = createMcpExpressApp();

app.post("/mcp", async (request, response) => {
  response.setHeader("Cache-Control", "no-store");

  const server = createWeatherMcpServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });

  await server.connect(transport);

  response.on("close", () => {
    void transport.close();
    void server.close();
  });

  await transport.handleRequest(request, response, request.body);
});

Keep secrets in the platform env; never bake API keys into the bundled HTML.

app.get("/health", (_request, response) => {
  response.json({ status: "ok", service: "weathersg" });
});

app.get("/ready", (_request, response) => {
  const ready = existsSync("dist/web/weather-app.html");
  response.status(ready ? 200 : 503).json({
    status: ready ? "ready" : "not_ready",
  });
});

Prefer streamable HTTP for hosts that expect a durable MCP session URL.

const port = Number(process.env.PORT ?? 3000);
const app = createWeatherHttpApp();

app.listen(port, "0.0.0.0", () => {
  console.log(`WeatherSG listening on ${port}`);
});

Container health checks must hit the MCP endpoint, not only a static page.

npm run dev

Host registration is configuration—retest after every URL or auth change.

npx @modelcontextprotocol/inspector@latest

Treat this as a checkpoint: protocol tests should pass before any chat-host wiring.

git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3000/mcp"]' npm start

Keep secrets in the platform env; never bake API keys into the bundled HTML.

npm run typecheck
npm test
npm run build

Prefer streamable HTTP for hosts that expect a durable MCP session URL.

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s \
  CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
CMD ["node", "dist/src/index.js"]

Container health checks must hit the MCP endpoint, not only a static page.

docker build -t weathersg .
docker run --rm -p 3000:3000 weathersg

Host registration is configuration—retest after every URL or auth change.

gcloud auth login
gcloud config set project YOUR_PROJECT_ID
gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com

Treat this as a checkpoint: protocol tests should pass before any chat-host wiring.

gcloud run deploy weathersg \
  --source . \
  --region asia-southeast1 \
  --allow-unauthenticated \
  --set-env-vars PUBLISHER_NAME=WeatherSG

Keep secrets in the platform env; never bake API keys into the bundled HTML.

https://YOUR_SERVICE_HOST/mcp

Prefer streamable HTTP for hosts that expect a durable MCP session URL.

curl -fsS https://YOUR_SERVICE_HOST/health
curl -fsS https://YOUR_SERVICE_HOST/ready

Container health checks must hit the MCP endpoint, not only a static page.

Is it raining anywhere in Singapore right now?

Show me the Singapore weather map.

Now show only rainfall stations.

Host registration is configuration—retest after every URL or auth change.

https://YOUR_SERVICE_HOST/mcp

Treat this as a checkpoint: protocol tests should pass before any chat-host wiring.

Test before the chat UI

Exercise tools and resources with an MCP inspector or scripted client. Chat hosts amplify failures; local protocol green saves hours.

Deploy and register

Ship the container to Cloud Run (or equivalent), then point ChatGPT and Claude connectors at the public MCP URL with the required auth headers. Re-verify the map interaction after TLS termination.

Closing

An MCP App is a product surface: contract, tools, UI resource, transport, container, and host config. Miss any layer and the demo dies in someone else's client.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Version API contracts next to embedder ids.

Separate HTTP handlers from pure services for unit tests.

Dark/light UI themes must not fork retrieval logic.

Log fetch ids with chat turns for later triage.

Rebuild indexes on a cadence matching source churn.

Refuse when top segments look weak.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.

Keep fixtures, owners, and rollback notes beside ingest and deploy changes.