Skip to content
Arif Dewi
Engineering Lead · Staff Full Stack / AI Engineer
Hiring me
Case study · AI product & developer tooling

An AI-first SDK for an energy company

A monolithic industrial control application turned into a modular SDK that engineers and LLM agents both build against, from typed contracts instead of guesses.

Engineering Lead · Staff Full Stack / AI Engineer · Energy company (contract, name under NDA)·2024–present

At a glance

Role
Engineering Lead · Staff Full Stack / AI Engineer. Owner of the frontend architecture and the AI assistant; I still write code daily.
Engagement
Independent contractor, remote from Dubai
Timeframe
Aug 2024 – present
Team
I lead the frontend team of 4–6
Stack
TypeScript 7 · UI kit with strict prop contracts · CLI for LLM agents · MCP server · Vercel AI SDK · Amazon Bedrock · LangGraph / LangChain · NestJS
  • ◆Test coverage 0 → 92%
  • ◆CI 10–12 min → under 1 min after migrating the framework to TypeScript 7
  • ◆In daily use across the engineering org

Context

An energy company ran its station operations through one monolithic control application. Every new screen meant working inside it, so delivery was slow, and every new engineer had to infer what a component expected by reading its source.

The company is now moving from that one application to a platform of many applications built on a shared SDK. Models were becoming part of how the team wrote code, and a model asked to build a screen against the monolith had the same problem as a new hire, only worse: it guesses confidently.

The brief I took on had two halves. Make the UI layer something engineers and their agents can build against from real contracts. And give operators an AI assistant that helps run the station without ever acting on it alone.

Constraints

  • ›Safety: this is an industrial control system, so a wrong action has physical consequences. The model may only propose commands; every command needs two human approvals, operator and admin, before it executes.
  • ›Legacy: the monolith keeps running station operations throughout the migration, so the SDK has to be extracted from it, not built beside it.
  • ›Team: a frontend team of 4–6, with me leading it and still writing production code.
  • ›Agents: anything an agent builds against has to be machine-readable. If the contract only lives in source code, the model starts guessing.

Architecture

monolithSDKUI kitprop contractscomponent metadataAPIHTTPCLIcomponents, for agentsMCP servermetadata, any MCP clientengineers + agentsread the contractsscreenscomposed at build timeoperator chatmodel proposes onlyoperator ✓ · admin ✓two human approvalsstationexecutes approved commands only

Components live in a UI kit, and each one carries a strict prop contract plus machine-readable metadata describing what it accepts. Agents reach that metadata two ways: CLI tooling built for LLM agents, which is where an agent assembles an application against the SDK, and an MCP server that exposes the same component metadata to any MCP-capable client. Documentation and agent skills ship with the SDK, so a model reads the docs the engineers read. On the operations side, the backend, the model and the agent harness existed as separate pieces; I wrote the interface layer that joins them, which the operator chat and the SDK tooling both stand on. Screens are composed at build time, never generated live in the product.

A prop contractIllustrative. Component names are invented.
export interface ReadingCardProps {
  /** Human label, shown above the value. */
  label: string;
  /** Latest reading. null renders the stale state, never 0. */
  value: number | null;
  unit: 'bar' | 'MW' | '°C' | '%';
  thresholds?: { warn: number; alarm: number };
  onAcknowledge?: (alarmId: string) => void;
}
Its machine-readable metadataWhat an agent reads instead of the component source.
{
  "component": "ReadingCard",
  "package": "@sdk/ui-kit",
  "summary": "One live reading with its unit, thresholds and stale state.",
  "props": {
    "label":         { "type": "string", "required": true },
    "value":         { "type": "number | null", "required": true,
                       "note": "null = stale; never substitute 0" },
    "unit":          { "type": "enum", "values": ["bar", "MW", "°C", "%"], "required": true },
    "thresholds":    { "type": "{ warn: number; alarm: number }", "required": false },
    "onAcknowledge": { "type": "(alarmId: string) => void", "required": false }
  },
  "rules": ["Data comes from a typed hook; the component never fetches."]
}
An MCP tool call an agent makesIllustrative tool name. The agent asks what a component accepts before it writes a line.
→ { "jsonrpc": "2.0", "id": 7, "method": "tools/call",
    "params": { "name": "get_component",
                "arguments": { "name": "ReadingCard" } } }

← { "jsonrpc": "2.0", "id": 7,
    "result": { "content": [{ "type": "text",
      "text": "{ \"component\": \"ReadingCard\", \"props\": { … } }" }] } }

Key decisions

DecisionWhyTrade-off
Strict prop contracts and machine-readable component metadataA model reads what a component accepts instead of inferring it from source, which is exactly the inference the monolith forced on everyone.Every component carries a contract that has to be kept accurate as it changes, and that upkeep is part of the definition of done.
CLI for agents first, MCP server secondThe CLI is where an agent builds an application against the SDK, so it had to exist first. The MCP server is thin enough to follow once there was something worth exposing.Teams on other MCP clients waited for the second surface.
Keep the model on the proposing side of every station commandOn an industrial system the cost of a wrong action is physical. Human sign-off makes that risk explicit instead of hoping the model gets it right.Every command waits on human approval. Slower by design.
Documentation and agent skills as first-class SDK deliverablesAgents are only as good as what they read; the same docs serve engineers and models.Docs compete with features for the same engineering time.
Raise the bar for the whole team, not only the SDKMigrating the framework to TypeScript 7, taking test coverage from 0 to 92% and cutting CI to under a minute means a change gets feedback while the engineer is still thinking about it.The migration and the test backfill took time up front, before any new feature shipped.

Outcome

  • ◆Test coverage from 0 to 92%, the bar I set for the team.
  • ◆CI from 10–12 minutes to under a minute after migrating the framework to TypeScript 7.
  • ◆400+ tech-debt issues identified and resolved.
  • ◆In daily use across the engineering org.
  • ◆The team also uses the AI code-review GitHub App I built, which has its own case study.

What I'd do differently

  • ›Cut the scope earlier. We built separate frontend and backend showcases, and support for several frontend frameworks when only one is in use. That was breadth hedging against a product intent nobody had pinned down, and all of it had to be built, tested and documented.
  • ›Force the product-intent conversation before writing code instead of absorbing the ambiguity as extra surface area. The build order I would keep: the CLI first, then the thin MCP server.