Upscale, Resize, and Transform Images Inside Claude and Cursor with MCP — No Code Required

Low-Res Images, High-Res Problems

You’re reviewing a pull request and notice the product images are 300×300. The design spec says 1200×1200. The originals are gone — the supplier sent what they sent, and nobody kept higher-resolution versions.

You could download the images, run them through a Python upscaling script, install the dependencies, wait for the model to process, and re-upload. Or you could ask your AI assistant to do it in one sentence.

MCP makes this possible. The Model Context Protocol lets AI assistants like Claude Desktop and Cursor call external APIs as tools. Connect the Image Transformation API as an MCP server, and your assistant can upscale, resize, crop, and convert images directly from a conversation. Describe what you need, it calls the API, you get the result.

No scripts. No dependencies. No context switching.

What MCP Is

Model Context Protocol is an open standard that gives AI assistants the ability to discover and call external services. Think of it as a plugin system — you register a tool, and the assistant uses it when your request matches.

When you connect the Image Transformation API as an MCP server, your assistant gains a full suite of image operations: upscaling low-resolution images at 2x, 3x, or 4x, resizing to exact dimensions, cropping, and converting between formats. All from a single MCP server.

The upscaling capability is not interpolation. The API uses an AI super-resolution model that generates actual detail — texture, edges, fine patterns — that didn’t exist in the original pixels. A blurry 500×500 image at 4x becomes a sharp 2000×2000 image with real visual information.

Setting Up in Claude Code

Claude Code supports MCP servers natively. Add the Iteration Layer server with a single command:

claude mcp add iterationlayer --transport http https://api.iterationlayer.com/mcp

The first time you use an Iteration Layer tool, a browser window opens for OAuth authentication. Log in, authorize access, and you’re connected. No API keys to manage.

To verify the server is available, start a conversation and ask Claude Code what MCP tools it has access to. You should see transform_image listed among the available tools.

Setting Up in Cursor

Add to your .cursor/mcp.json:

{
  "mcpServers": {
    "iterationlayer": {
      "type": "http",
      "url": "https://api.iterationlayer.com/mcp"
    }
  }
}

Save and restart. The Image Transformation tool is now available in your Cursor AI conversations. Authentication works the same way — OAuth in the browser on first use.

Using It Conversationally

Upload an image and describe what you need:

“Upscale this product image to 4x resolution.”

The assistant calls the Image Transformation API with an upscale operation at factor 4, and returns the upscaled image. A 500×500 input becomes 2000×2000 with generated detail — not blurred pixels.

“This logo is too small for the print layout. Upscale it to 2x.”

The assistant uses a 2x factor, doubling the resolution while preserving the logo’s sharpness.

“Upscale this photo to 3x for the hero banner.”

Three times the resolution. The model generates texture and edge detail that makes the image hold up at the larger size.

More MCP Conversations

The conversational pattern scales beyond single-image requests. Here are a few more interactions that show what’s possible.

Comparing factors side by side:

“Upscale this photo at 2x and 4x so I can compare the results.”

The assistant makes two separate API calls — one with factor 2, one with factor 4 — and returns both images. You compare them in the conversation and pick the one that balances file size and quality for your use case.

Upscaling a batch of icons:

“I have these five SVG-exported PNGs at 64×64. Upscale each one to 4x for the app store assets.”

The assistant processes each image individually through the API. Five calls, five results, all in one conversation thread. No script, no loop, no dependencies to install.

Chaining multiple operations:

Because the Image Transformation MCP server handles upscaling, resizing, cropping, and format conversion in a single server, you can chain operations naturally:

“Upscale this image to 3x, then convert it to WebP and resize it to exactly 1200×800.”

The assistant chains the operations through the Image Transformation API — upscale, format conversion, and resize — all in one conversation.

“This supplier photo is 400×300. Upscale it to 4x, then crop it to a square and compress it under 200KB.”

The assistant runs the upscale (producing a 1600×1200 image), then chains a crop and a target-size compression. What would normally be three scripts or a multi-step CLI pipeline becomes one sentence.

When to Upscale

Legacy images. Old product catalogs, archived marketing materials, images from systems that stored thumbnails as the “original.” The high-res version doesn’t exist. AI upscaling is the only way to recover usable resolution.

Supplier-provided images. E-commerce teams deal with this constantly. Suppliers send images at whatever resolution they have — often 300×300 or 400×400. Marketplaces require 1000×1000 minimum. Traditional upscaling produces blurry results. AI upscaling generates the detail needed to meet the spec.

Print preparation. An image looks fine on screen at 72 DPI but falls apart at 300 DPI for print. Upscaling 2x or 4x gives you enough resolution for print-quality output without visible artifacts.

User uploads. Users upload whatever they have — phone screenshots, cropped thumbnails, profile pictures from 2015. If your application needs a larger version, AI upscaling is better than showing a blurry stretched image.

Choosing a Factor

The API supports three upscaling factors. Each one multiplies both dimensions:

  • 2x — a 500×500 image becomes 1000×1000 (4x the pixels). Good for images that are close to the target resolution but need a bump.
  • 3x — a 500×500 image becomes 1500×1500 (9x the pixels). A solid middle ground for most use cases.
  • 4x — a 500×500 image becomes 2000×2000 (16x the pixels). Maximum enhancement. Best for images that are significantly below the target resolution.

Higher factors mean more generated detail. The model is filling in information that wasn’t in the original, so there’s a practical limit — upscaling a 50×50 icon to 4x won’t produce a photo-realistic 200×200 image. For best results, start with the smallest factor that gets you to the resolution you need.

What Happens Under the Hood

When the assistant calls the API, it sends a POST request to https://api.iterationlayer.com/image-transformation/v1/transform with your image and the desired operations. The request body specifies the file and a list of operations — for example, { "file": {"type": "url", "name": "image.jpg", "url": "..."}, "operations": [{"type": "upscale", "factor": 4}] }. The AI model processes the image, generating new pixels based on patterns it learned from millions of high-resolution images.

You can chain multiple operations in a single request — upscale, then resize, then convert to WebP — and the API processes them in sequence. This is what makes the single MCP server so versatile: one connection gives you the full image transformation pipeline.

Quality Expectations

AI upscaling is not magic, and setting expectations correctly matters. The model works best on photographs and natural images — textures, skin, hair, fabric, foliage. These have predictable patterns the model has learned to extend.

It works well on product photography. Supplier images that arrive at 400×400 upscale to 1600×1600 with believable detail. Edges stay sharp, product surfaces gain texture, and the result passes visual inspection at marketplace-required dimensions.

It works less well on text and sharp geometric shapes. A screenshot with small text at 300×200 upscaled to 4x will have smoother text, but it won’t match the crispness of a native-resolution render. For images with prominent text, 2x is usually the practical ceiling before artifacts become noticeable.

Icons and logos fall somewhere in between. Simple shapes with flat colors upscale cleanly. Detailed logos with fine lines may gain subtle softness at higher factors. If you need pixel-perfect scaling for vector-origin artwork, re-exporting from the source SVG at the target resolution is always better — but when the source file is gone, 2x upscaling is a reasonable recovery option.

MCP vs Direct API Calls

MCP is built for ad-hoc work. You need one image transformed, right now, without writing code. The assistant handles the API call, the file encoding, the response parsing. You describe, it delivers.

For production pipelines — upscaling product images during catalog import, processing user uploads on the fly, batch-enhancing legacy image libraries — use the API directly. The request body is JSON. Any language that can make an HTTP POST can transform images.

The two approaches fit different moments. Prototype in conversation with MCP, then move the pattern into your codebase when it becomes a recurring workflow.

Common Pitfalls

Upscaling already-compressed JPEGs. Heavy JPEG compression creates visible block artifacts. Upscaling amplifies those artifacts rather than removing them. If the source image is heavily compressed, the upscaled version will look better than bicubic interpolation, but the compression artifacts will still be visible. Start from the highest-quality source you have.

Using 4x when 2x is enough. Higher factors produce larger files and longer processing times. If you need a 1000×1000 image and your source is 600×600, 2x gives you 1200×1200 — more than enough. Save 4x for cases where the source is genuinely small relative to the target.

Expecting upscaling to fix motion blur or focus issues. AI upscaling generates detail from patterns, but it cannot recover information that was never captured. A blurry photo becomes a larger blurry photo. The model may sharpen some edges, but fundamental focus problems remain.

Get Started

Sign up for a free account at iterationlayer.com — no credit card required. Run the claude mcp add command above, authenticate in the browser, and transform your first image from your next Claude Code or Cursor conversation.

The docs cover the full API reference — request format, response format, file size limits, supported operations, and factor options.

How We Built Our Pitch Deck with Our Own API

Hero slide

The Problem with Slide Decks

We needed marketing slides. The kind you send to potential partners, drop into a pitch deck, or post on social media. Ten slides covering the product, features, integrations, use cases, and a CTA.

The obvious path: open Figma, drag boxes around, export PNGs. Update copy? Back to Figma. New product launched? Back to Figma. Font change? Back to Figma for every slide.

We already had an API that composites images from JSON layers. We already had layout layers that arrange children with gap, alignment, padding, and border radius. The slides were just images with text and colored boxes.

So we built them with our own API.

What We Built

Ten slides, generated from code, pixel-identical every time:

Hero slide

Problem slide

Product slide for Document Extraction

Integrations slide

Recipes slide

CTA slide

Each slide is a single API call. The canvas is 2540×1520 (2x resolution for crisp output on retina screens). The whole deck generates in under 2 seconds.

Layout Layers Are the Key

Most of the slides use layout layers — a layer type that arranges children horizontally or vertically, like CSS flexbox. You set direction, gap, alignment, padding, border radius, and background color. Children auto-size or use fixed dimensions.

This is what makes the slides possible without manual pixel positioning. A trust badge pill is a horizontal layout with an icon and auto-width text:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const trustPill = (iconBase64: string, label: string) => ({
  type: "layout" as const,
  direction: "horizontal" as const,
  gap: 8,
  vertical_alignment: "center" as const,
  background_color: "#38bdf8",
  padding_top: 10,
  padding_bottom: 10,
  padding_left: 18,
  padding_right: 22,
  border_radius: 20,
  layers: [
    {
      type: "image" as const,
      file: { type: "base64", name: "icon.svg", base64: iconBase64 },
      dimensions: { width: 18, height: 18 },
    },
    {
      type: "text" as const,
      text: label,
      font_name: "PlusJakartaSans",
      font_size_in_px: 15,
      font_weight: "bold",
      text_color: "#FFFFFF",
      dimensions: { width: "auto", height: 22 },
    },
  ],
});

const result = await client.generateImage({
  dimensions: { width: 2540, height: 1520 },
  layers: [
    { index: 0, type: "solid-color", hex_color: "#0ea5e9" },
    {
      index: 1,
      type: "layout",
      direction: "horizontal",
      gap: 24,
      position: { x: 112, y: 430 },
      layers: [
        trustPill(shieldIconBase64, "Zero data retention"),
        trustPill(flagIconBase64, "Made & hosted in the EU"),
        trustPill(cardIconBase64, "No credit card required"),
      ],
    },
  ],
});

The "auto" width on the text layer is what makes this work. The text renders at its natural width, then the layout wraps it with padding and a background. No measuring, no calculating pixel offsets.

Feature Cards with Background Layers

The product slides show feature cards in a 2×3 grid. Each card is a vertical layout with an icon, title, and description — positioned on the canvas with fixed dimensions:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const featureCard = (iconBase64: string, title: string, description: string) => ({
  type: "layout" as const,
  direction: "vertical" as const,
  gap: 32,
  background_color: "#0284c7",
  padding: 56,
  border_radius: 32,
  layers: [
    {
      type: "image" as const,
      file: { type: "base64", name: "icon.svg", base64: iconBase64 },
      dimensions: { width: 56, height: 56 },
    },
    {
      type: "text" as const,
      text: title,
      font_name: "PlusJakartaSans",
      font_size_in_px: 44,
      font_weight: "bold",
      text_color: "#FFFFFF",
      dimensions: { width: 668, height: 68 },
    },
    {
      type: "text" as const,
      text: description,
      font_name: "PlusJakartaSans",
      font_size_in_px: 36,
      text_color: "#e0f2fe",
      dimensions: { width: 668, height: 140 },
    },
  ],
});

const result = await client.generateImage({
  dimensions: { width: 2540, height: 1520 },
  layers: [
    { index: 0, type: "solid-color", hex_color: "#0ea5e9" },
    {
      index: 1,
      ...featureCard(iconBase64, "Schema-Driven Extraction", "Define 17 typed fields and get structured JSON back."),
      dimensions: { width: 780, height: 480 },
      position: { x: 112, y: 480 },
    },
  ],
});

When a card has dimensions set, the layout clips to that size and the border_radius is applied to the fixed container — not the auto-sized content. This means cards in a grid all have identical dimensions regardless of content length.

Centering with Fixed Dimensions

The CTA slide centers the trust badges horizontally. Layout layers don’t center children by default — they stack from the start. But when you set dimensions to the full slide width and horizontal_alignment to "center", the layout positions the auto-sized content in the middle:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const result = await client.generateImage({
  dimensions: { width: 2540, height: 1520 },
  layers: [
    { index: 0, type: "solid-color", hex_color: "#0ea5e9" },
    {
      index: 1,
      type: "layout",
      direction: "horizontal",
      horizontal_alignment: "center",
      gap: 24,
      dimensions: { width: 2540, height: 100 },
      position: { x: 0, y: 860 },
      layers: [
        trustPill(shieldBase64, "Zero data retention"),
        trustPill(flagBase64, "Made & hosted in the EU"),
        trustPill(cardBase64, "No credit card required"),
      ],
    },
  ],
});

CTA slide with centered badges

This is the same pattern you’d use for centering a button, a logo, or any group of elements on a slide.

The Generative Wave Background

Every slide shares the same wave background — a deterministic SVG pattern seeded by a fixed string. The algorithm stacks wave bands with varying amplitude and color, rendered as Catmull-Rom spline paths. We covered the wave generator in How We Generate OG Images with Our Own API.

The wave SVG is rendered as a low-opacity image layer on top of a solid sky-blue background. These two layers appear at the bottom of every slide:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const waveSvgBase64 = btoa(generateWaveSvg("marketing-slides", 2540, 1520));

const result = await client.generateImage({
  dimensions: { width: 2540, height: 1520 },
  layers: [
    { index: 0, type: "solid-color", hex_color: "#0ea5e9" },
    {
      index: 1,
      type: "image",
      file: { type: "base64", name: "waves.svg", base64: waveSvgBase64 },
      position: { x: 0, y: 0 },
      dimensions: { width: 2540, height: 1520 },
      opacity: 10,
    },
  ],
});

Same input, same output. The slides are byte-identical across runs.

Why Not Figma

A few reasons:

  • Copy changes are code changes. A headline update is a string edit, not a design tool session. It goes through code review like everything else.
  • New products are automatic. When we launched Sheet Generation, the slide appeared in the deck without anyone opening a design tool. The product data already existed — the slide just rendered it.
  • Brand consistency is enforced by code. Every slide uses the same padding, the same font sizes, the same color tokens. There’s no “someone forgot to update the font on slide 7” problem.
  • It’s version controlled. Every slide change is a git commit with a diff you can read.

The tradeoff is that design iteration is slower. You can’t drag a box and see the result instantly. But for slides that follow a template — which most marketing slides do — the code-first approach is faster after the initial setup.

Try It

The Image Generation API docs cover all layer types including layouts. The Generate Product Slide recipe shows a complete slide with pills, feature lists, and a CTA button.

If you’re generating images that follow a pattern — slides, social cards, certificates, tickets — layout layers replace the pixel math with structured composition. Define the structure once, swap the data.

Why 88% of MCP Servers Have No Real Authentication (And How to Fix It)

AI agents are accessing databases, sending emails, calling APIs, and making purchases. But there’s no standard way to identify them, limit what they can do, or trace their actions back to a human.

I dug into the numbers:

  • 88% of MCP servers need authentication
  • Only 8.5% use OAuth
  • 53% rely on static API keys in environment variables
  • 80% of organizations can’t tell what their agents are doing in real-time

This is the wild west. So I built AgentsID to fix it.

The Problem

When you build an MCP server, every tool is wide open by default. Any agent with the API key can call any tool — search, delete, deploy, admin reset — with zero restrictions.

There’s no way to:

  • Give Agent A access to search but block delete
  • Know which agent made which tool call
  • Trace an agent’s actions back to the human who authorized it

The Fix: 3 Lines of Middleware

Install the SDK:

npm install @agentsid/sdk

Add the middleware:

import { createHttpMiddleware } from '@agentsid/sdk'; 

const guard = createHttpMiddleware({
  projectKey: process.env.AGENTSID_PROJECT_KEY,
});

Validate every tool call:

const auth = await guard.validate(token, toolName);
if (!auth.permission.allowed) { 
  return { error: 'Blocked', reason: auth.permission.reason };
}

That’s it. Every tool call is now validated.

What You Can Control

AgentsID uses a deny-first model. Everything is blocked unless you explicitly allow it. The permission engine supports 14 constraint types:

Access — Allow/deny by tool name with wildcards (search_* allowed, delete_* blocked)
Time & Rate — Restrict to business hours, limit calls per minute/hour
Behavioral — Require tools to run in sequence, detect anomalous behavior
Resource — Set budget caps, limit session duration
Governance — Require human approval for sensitive actions, limit delegation depth

Delegation Chains

When Agent A spawns Agent B, permissions automatically narrow. Agent B can never have more access than Agent A. Revoke the parent and the entire chain downstream stops.

Audit Trail

Every tool call — allowed or denied — is logged. You get a full record of what each agent did, when, and why it was allowed or blocked. The dashboard shows it all in a live feed.

Getting Started

npm install @agentsid/sdk    # TypeScript
pip install agentsid          # Python
gem install agentsid          # Ruby

Free tier: 25 agents, 10,000 events/month. No credit card.

Wikipedia bans AI-generated content

The Wikipedia community has officially moved to ban the use of AI-generated content across its platform. As reported on March 27, 2026, this policy shift comes after an extensive debate regarding the risks that large language models pose to the encyclopedia’s core standards of verifiability and neutral point of view. By prioritizing human-led research over automated text, Wikipedia aims to protect its readers from “hallucinations” and ensure that every claim remains grounded in reliable, human-curated sources. This decision marks a significant boundary in the evolution of the internet, reaffirming Wikipedia’s commitment to human oversight in an increasingly automated information landscape.

The primary motivation for this restriction lies in the fundamental incompatibility between generative AI and Wikipedia’s core editorial pillars. LLM frequently produce hallucinations, which are statements that appear factual but are actually fabricated or lack a reliable source (Wikipedia, 2026). These inaccuracies directly violate the “Verifiability” policy, as AI often invents citations or misinterprets complex data. Furthermore, the Wikipedia community determined that the sheer volume of low quality content generated by AI could overwhelm volunteer editors and degrade the reliability of the platform (The Guardian, 2026). Beyond mere errors, there are deep concerns regarding algorithmic bias and the potential for AI to mirror existing societal prejudices, which compromises the “Neutral Point of View” that readers expect.

While the ban is broad, it is not an absolute exile of all automated tools, as the policy provides specific exceptions for linguistic refinement. Editors are still permitted to use AI for copyediting, which includes fixing grammar or typos, as long as the tool does not introduce any new information or citations (Wikipedia, 2026). Additionally, assisted translations between different language versions of the site remain acceptable, provided the human editor is fluent in both languages and manually verifies every sentence. Ultimately, the burden of accuracy rests solely on the individual contributor, because Wikipedia maintains that human editors are legally and editorially responsible for all text they publish (The Guardian, 2026). This ensures that while the process might be faster, the eyes on the page remain human and accountable.

Wikipedia’s decision to restrict AI generated content marks a pivotal moment in the ongoing struggle for digital information quality. In an era where automated “slop” threatens to saturate the internet, this community led stance reaffirms that human curation remains the gold standard for reliable and verified knowledge (The Guardian, 2026). While automation might offer speed, the platform’s commitment to verifiable and neutral information ensures its longevity as a trusted resource (Wikipedia, 2026). Ultimately, this policy highlights that while machines can process data, they cannot replicate the nuanced understanding and moral accountability of a human volunteer.

References

Milman, O. (2026, March 27). Wikipedia bans AI-generated content in its online encyclopedia. The Guardian; The Guardian. https://www.theguardian.com/technology/2026/mar/27/wikipedia-bans-ai

Wikipedia Contributors. (2026, March 27). Wikipedia:Writing articles with large language models. Wikipedia; Wikimedia Foundation.

‌

Why We Built Iteration Layer

Content Processing Is a Mess

If you’ve built anything that touches documents or images, you know the drill. You need to extract data from PDFs, so you duct-tape together an OCR library and a regex parser. You need thumbnails, so you spin up ImageMagick in a Docker container. You need to generate reports or ebooks, so you wrestle with PDF libraries that treat a simple table like a research problem.

Each tool solves one narrow problem. Each one breaks in its own way. And the glue code connecting them — the format conversions, the error handling, the retry logic — that’s where the real complexity lives. Not in the business logic you actually care about.

We’ve been on both sides of this. Before Iteration Layer, we built an AI-driven book publishing company. That meant building the entire content pipeline from scratch: parsing manuscripts, generating book covers programmatically, processing product images for Amazon, rendering marketing graphics for launches. Every piece worked. Every piece also broke in its own creative way at 2 AM before a release.

We maintained the Sharp pipeline that worked perfectly until someone uploaded a CMYK TIFF. We wrote the template engine that couldn’t handle a font weight it hadn’t seen before. We built the manuscript-to-EPUB converter that choked on tables. And every time we fixed one thing, we thought: this should just be an API call.

So we built it.

One Pipeline, Four APIs

Content processing follows a natural lifecycle: you ingest raw content, you transform it, and you generate new output from it. Instead of building one monolithic platform that tries to do everything, we built four focused APIs that map to this lifecycle.

Document Extraction handles ingestion. Give it a PDF, a Word document, a scanned image — it gives you structured JSON. You define a schema describing the fields you want, and the parser extracts them with confidence scores so you know when to trust the result and when to flag it for review. No OCR setup, no template configuration, no regex. It handles field types like text, numbers, dates, addresses, IBANs, currencies, arrays, and more out of the box.

Image Transformation covers the middle of the pipeline — the part where you need to upscale, resize, crop, convert formats, adjust quality, or chain multiple operations together. Define up to 30 operations in a single request: upscale to 4x resolution, resize to 800×600, convert to WebP, compress to 85% quality, smart-crop around the detected subject. One API call instead of a Sharp pipeline you have to host and scale yourself. Need an image under 500 KB for email? Tell the API the target file size and it figures out the optimal quality and dimension tradeoffs.

Image Generation turns templates into pixels. Write your template in HTML and CSS — technologies you already know — inject dynamic data, and get back a PNG, JPEG, or PDF. Social cards, certificates, OG images, report graphics. Anything you’d design in Figma and then manually export fifty times, you can now render programmatically.

Document Generation closes the loop on the output side. Feed it structured data and get back a polished PDF, DOCX, EPUB, or PPTX. Contracts, reports, ebooks, slide decks — generated from templates, populated with your data, ready to ship.

Each API works independently. You can use the Document Extraction without ever touching image processing. You can generate documents without parsing a single one first. But the real leverage comes when you chain them together.

Composable by Design

The output of one API flows naturally into the input of the next. Parse a supplier catalog, feed each product into Image Generation, and you have marketplace-ready listing images — no glue code, no format juggling. Parse manuscripts, transform the cover art, generate the final EPUB. Extract article text, transform the hero image, generate a social card.

This is the Unix philosophy applied to content processing: small, focused tools that compose into workflows you haven’t imagined yet.

That composability is a deliberate choice, and it cuts against the grain. Monolithic platforms lock you into one vendor’s idea of a workflow. If their OCR is good but their image processing is mediocre, tough luck — you’re stuck with both or neither.

We think APIs should work like building blocks. Snap them together however you want. Combine ours with your own services or third-party tools. The output is always standard JSON or image data — nothing proprietary, nothing locked in.

This also means you pay for what you use. Need parsing this month but not image generation? You only pay for parsing. A spike in document parsing doesn’t affect your image generation capacity. Start with one API, add another when the need arises, swap one out without touching the rest.

MCP: APIs as Agent Tools

Every Iteration Layer API ships as an MCP server from day one. If you work with Claude, Cursor, or any MCP-compatible client, our APIs show up as tools your agent can call directly.

That changes the workflow fundamentally. Instead of writing integration code, you describe what you want. “Parse this supplier catalog and generate a product card for each item.” The agent discovers the Document Extraction, calls it with the right schema, takes the structured output, and feeds it into Image Generation — all without you specifying the sequence.

We’ve been using this internally while building the platform, and it’s the kind of thing that feels like a gimmick until you try it. Once you’ve watched an agent chain three API calls to solve a task you were about to spend an hour scripting, it’s hard to go back.

Building Blocks for What’s Next

The four APIs we’re launching cover the core of the content lifecycle, but we see them as the foundation, not the finish line.

Our vision is a library of focused, composable APIs that cover every step of content processing — from raw input to polished output. Each one does one thing well. Each one connects to everything else. The same way Unix pipes let you chain grep | sort | uniq into something no single tool could do, we want you to be able to chain Ingest, Transform, and Generate steps into workflows that solve your specific problem.

That means more APIs, more integrations, more field types, more output formats. But always following the same principles — focused scope, standard I/O, and the ability to snap into any pipeline.

We’re building infrastructure that disappears. You shouldn’t have to think about OCR libraries, image processing servers, or headless browser farms. You should think about your product, your users, and the content workflow that connects them. The plumbing is our problem.

Get Started

Pick the API that solves your most immediate problem — Document Extraction, Image Transformation, Image Generation, or Document Generation. We also publish TypeScript and Python SDKs, so your next npm install or pip install is all the setup you need — and if you’re building with AI agents, the SDKs make our APIs discoverable as tools out of the box.

Sign up for a free account — no credit card required. As new needs come up, add another API to the chain. They all use the same authentication, the same error format, and the same response structure, so adding a second or third takes minutes.

We built this because we needed it. We think you might too.

Skip the Designer — Editing Logic Apps Data Mapper LML Files Directly

The Azure Logic Apps Data Mapper has a visual designer in VS Code. You drag lines between schemas, drop in functions, and it generates an .lml file (YAML) that compiles to XSLT 3.0.

In theory, you never touch the YAML. In practice, the designer has reliability problems that make direct LML editing the better workflow.

Why skip the designer?

The designer doesn’t always persist changes correctly:

  • Changes revert on reopen — save, close, reopen, and the .lml on disk has the old value
  • String literals lose inner quotes — xpath("'kg'") becomes xpath("kg"), changing a literal to an element reference
  • Expressions get corrupted — conditions like xpath("if (...) then 'Y' else 'N'") lose quote characters
  • Function arguments get rewritten — argument order or paths change silently
  • No undo across sessions — you need git diff to catch silent corruption

These happen with normal operations, not edge cases.

What LML looks like

LML is YAML. Here’s a complete map:

$version: 1
$input: XML
$output: XML
$sourceSchema: OrderSchema.xsd
$targetSchema: ShipmentOutput.xsd
$sourceNamespaces:
  ns0: http://schemas.contoso.com/Order
$targetNamespaces:
  xs: http://www.w3.org/2001/XMLSchema
Shipments:
  Shipment:
    ShipmentRef: prefixShipRef(/ns0:Order/ns0:OrderID)
    ShipDate: reformatDate(/ns0:Order/ns0:OrderDate)
    Recipient: /ns0:Order/ns0:Customer/ns0:CustomerName
    Lines:
      $for(/ns0:Order/ns0:Items/ns0:Item):
        $if(xpath("ns0:Quantity >= 1")):
          Line:
            Product: ns0:ProductName
            Quantity: ns0:Quantity

Header declares schemas and namespaces. Body maps target elements (left) to source expressions (right).

LML syntax cheat sheet

# Direct mapping
Recipient: /ns0:Order/ns0:Customer/ns0:CustomerName

# Attribute on parent element
$@shipId: /ns0:Order/ns0:OrderID

# Loop over repeating elements (paths inside are relative)
$for(/ns0:Order/ns0:Items/ns0:Item):
  Line:
    Product: ns0:ProductName

# Conditional
$if(xpath("ns0:Quantity >= 1")):
  Line:
    Product: ns0:ProductName

# Custom function (defined in Functions/*.xml)
ShipDate: reformatDate(/ns0:Order/ns0:OrderDate)

# Raw XPath escape hatch
Priority: xpath("upper-case(@priority)")
WeightUnit: xpath("'kg'")   # inner quotes = string literal

Custom extension functions

Defined in Artifacts/DataMapper/Extensions/Functions/*.xml:

<customfunctions>
  <function name="reformatDate" as="xs:string"
            description="Reformats yyyy-MM-dd to dd/MM/yyyy.">
    <param name="dateVal" as="xs:date"/>
    <value-of select="format-date($dateVal, '[D01]/[M01]/[Y0001]')"/>
  </function>
</customfunctions>

Gotchas

Zero-parameter functions crash the compiler. The SDK throws a NullReferenceException and silently skips the entire XML file. Workaround: add a dummy parameter.

Function arguments must be single-step element names. Multi-step paths cause compile errors — wrap them in xpath():

# INVALID
Address: formatAddress(ns0:Customer/ns0:Street, ns0:Customer/ns0:City)

# FIX
Address: formatAddress(xpath("ns0:Customer/ns0:Street"), xpath("ns0:Customer/ns0:City"))

The workflow: edit, compile, verify

1. Edit the YAML

Open the .lml file in any text editor and make your change.

2. Compile to XSLT

Install lml-compile, a dotnet global tool that wraps the Logic Apps SDK:

dotnet tool install -g lml-compile

Compile:

lml-compile Artifacts/MapDefinitions/MyMap-lml.lml Artifacts/Maps/MyMap-lml.xslt
# OK: MyMap-lml.xslt

3. Verify

Run dotnet test or press F5 with the XSLT Debugger extension.

Auto-compile on save

Install the Run on Save extension and add this to your workspace settings (for multi-root workspaces, add it in the .code-workspace file — Run on Save doesn’t read folder-level settings):

"emeraldwalk.runonsave": {
  "commands": [
    {
      "match": "\.lml$",
      "cmd": "lml-compile ${file} ${fileDirname}/../Maps/${fileBasenameNoExt}.xslt"
    }
  ]
}

Note: Run on Save uses its own variables — not VS Code task variables. ${fileBasenameNoExt} and ${fileDirname} work; ${fileBasenameNoExtension} and ${workspaceFolder} do not. Reload the window after adding the config.

Now the cycle is just: edit .lml → save → F5 or dotnet test. The compile happens automatically.

What the compiler catches

  • Malformed YAML / missing $version header
  • Missing schemas in Artifacts/Schemas/
  • Unrecognized function calls
  • Invalid XPath syntax

Errors surface on save instead of at deploy time.

Naming convention

Name hand-authored LML files with a -lml suffix:

Artifacts/MapDefinitions/OrderToShipment-lml.lml  →  Artifacts/Maps/OrderToShipment-lml.xslt

This avoids colliding with hand-authored XSLT files and makes it clear which files are compiled output.

Summary

Use the designer to scaffold the initial map. Then switch to direct LML editing for all subsequent changes.

Edit .lml → save → auto-compile → verify

No designer round-trip. No silent rewrites. No surprises.

The XSLT Debugger extension is on the VS Code Marketplace for macOS and Windows.

awesome-trending-repos: Modern Web Interface for GitHub Trending

CI/CD Pipeline

In my previous blog post, I introduced the awesome-trending-repos project. Back then, the project only wrote data to a README.md file. Things have changed. The project is now a fully functional modern web application.

CI/CD Pipeline
Automated build and deployment process with GitHub Actions

Why a Web Interface?

A README.md file works, but it’s static. To track trending projects effectively, I wanted a more interactive experience. Users needed to be able to filter by programming language, search repositories, and see data visualized with charts.

So I built a modern single-page application (SPA) with React + Vite.

Tech Stack

Frontend:

  • React 19 – Latest version
  • Vite – Fast dev server and build tool
  • Tailwind CSS v4 – Utility-first styling
  • Framer Motion – Animations
  • Recharts – Data visualization

Backend/Infrastructure:

  • GitHub Actions – Automation
  • GitHub Pages – Hosting
  • Custom Domain – furkankoykiran.com.tr

Project Structure

The core project structure:

awesome-trending-repos/
├── src/
│   ├── App.jsx          # Main React component
│   ├── main.jsx         # Entry point
│   └── index.css        # Tailwind + custom styles
├── scripts/
│   └── update.js        # Trending data scrape script
├── public/
│   ├── data/
│   │   └── trending.json # Frontend data source
│   └── logo.png
├── .github/workflows/
│   └── update-trends.yml # CI/CD pipeline
└── vite.config.js       # GitHub Pages config

GitHub Pages Integration

When using Vite with GitHub Pages, the most important configuration is the base path. If your project runs in a subdirectory (e.g., github.io/repo-name), you must specify this in vite.config.js:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
  base: '/awesome-trending-repos/', // GitHub Pages subdirectory
});

Without this setting, asset paths will break in production and you’ll get 404 errors.

GitHub Actions Workflow

The workflow runs every day at midnight UTC:

  1. Fetch trending data – Scrape GitHub Trending page
  2. Build frontend – Create dist/ folder with npm run build
  3. Commit data – Commit README and data files if changed
  4. Deploy – Distribute to GitHub Pages
# .github/workflows/update-trends.yml
name: Update and Deploy Trends

on:
  schedule:
    - cron: '0 0 * * *'  # Daily at midnight
  workflow_dispatch:

permissions:
  contents: write
  pages: write
  id-token: write

jobs:
  update-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run update script
        run: npm run update
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Build frontend
        run: npm run build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: './dist'

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: update-and-build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Deployment Flow
Automated deployment flow

Frontend Features

1. Language Filtering

Users can filter by programming language:

const languages = ['All', ...new Set(repos.map(r => r.language).filter(Boolean))];

const filteredRepos = repos.filter(repo => {
  const matchesLang = filter === 'All' || repo.language === filter;
  const matchesSearch = repo.name.toLowerCase().includes(searchQuery.toLowerCase());
  return matchesLang && matchesSearch;
});

2. Search Function

Real-time search across repo name, owner, and description.

3. Visual Statistics

Recharts library for language distribution bar chart and growth leaders list:

<ResponsiveContainer width="100%" height="100%">
  <BarChart data={data?.insights?.topLanguages || []}>
    <CartesianGrid strokeDasharray="8 8" stroke="#ffffff03" />
    <XAxis dataKey="language" stroke="#475569" />
    <YAxis stroke="#475569" />
    <Tooltip contentStyle={{ backgroundColor: 'rgba(15, 23, 42, 0.9)' }} />
    <Bar dataKey="count" radius={[12, 12, 0, 0]}>
      {data?.insights?.topLanguages?.map((entry, index) => (
        <Cell key={index} fill={getLangColor(entry.language)} />
      ))}
    </Bar>
  </BarChart>
</ResponsiveContainer>

4. Deployment Feed

A feed entry is created for each update. Users can see the deployment history.

Custom Domain Setup

To use a custom domain with GitHub Pages:

  1. Create CNAME file – Add your domain to public/CNAME:
   furkankoykiran.com.tr
  1. DNS setting – At your domain provider:
   CNAME furkankoykiran.github.io
  1. HTTPS – GitHub Pages automatically provides SSL with Let’s Encrypt.

Performance Tips

1. Asset Optimization

Vite production build automatically does minification and tree-shaking. For large images, consider using optimized images instead of putting everything in public/.

2. Lazy Loading

For large lists, consider virtual scrolling. Not needed for this project with 25 repos, but good to know if you scale.

3. Caching

GitHub Pages automatically adds cache headers. For index.html, use cache-busting:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: `assets/[name].[hash].js`,
        chunkFileNames: `assets/[name].[hash].js`,
        assetFileNames: `assets/[name].[hash].[ext]`
      }
    }
  }
});

Live Demo

The project is currently live at furkankoykiran.com.tr/awesome-trending-repos. It updates automatically every day.

Project Logo
Project logo

OG Image
Open Graph preview image

Conclusion

Moving from a README.md to a modern web application significantly improved the project’s usability. Users now have an interactive experience instead of just a static list.

The GitHub Pages + Vite combination is an excellent solution for static sites. Free, fast, and fully automated with CI/CD.

The project is fully open source. If you want to contribute, send a pull request to the GitHub repo.

Source: blog.furkankoykiran.com.tr

Introducing Flexibility Without Losing Structure in Clprolf

Why a structured approach doesn’t have to be a barrier to entry.

Introduction

When introducing a structured approach like Clprolf, a common concern appears quickly:

  • “Do I have to rewrite everything?”
  • “Does this change how I usually code?”
  • “Is this too strict for real-world usage?”

These concerns are legitimate.

Most real-world codebases are not perfectly structured.
They mix domain logic and technical details.
They evolve under constraints.

So the question becomes:

Can Clprolf exist without forcing a complete rewrite?

Starting From Existing Code

Clprolf is based on two simple principles:

  • a class is either technical or organized around a well-defined domain
  • inheritance must preserve that domain — otherwise, composition is used

These principles provide structure.

But they do not require starting from scratch.

You can start from existing code.

Even when a class mixes concerns,
it still has a primary role.

That role defines its nature.

A Flexible Entry Point

Clprolf can be used in a more flexible way by default, as the compiler operates in this mode.

In this mode:

  • existing code remains valid
  • no immediate refactoring is required
  • no additional annotations are necessary

The goal is not to enforce structure immediately,
but to make it available.

Structure Still Exists

Flexibility does not remove the model.

The same principles still apply.

Even in mixed code:

  • the primary role of a class remains identifiable
  • domain and technical concerns are still distinct in nature

Clprolf does not ignore structure.
It simply does not force it upfront.

Strict and Flexible Clprolf

This leads to two ways of using Clprolf:

  • a flexible usage, closer to common practices
  • a strict usage, with stronger structural guarantees

The difference is not in the model itself,
but in how strictly it is applied.

Strict Clprolf makes the separation explicit.
Flexible Clprolf allows it to remain implicit.

Compatibility Between Both

Both approaches are fully compatible.

  • strict code works in all cases
  • flexible code integrates without issue

There is no fragmentation.

Clprolf remains a single model,
regardless of how strictly it is applied.

Gradual Evolution

In practice, adoption becomes natural.

Developers can:

  • start with their existing code
  • observe where responsibilities become unclear
  • introduce structure where it brings value

No forced transition is required.

Clprolf can be adopted progressively.

Working With Existing Libraries

Clprolf does not require external code to follow its model.

Libraries, frameworks, and APIs can be used as they are.

What matters is how they are integrated.

The structure lives in your code,
not in the code you depend on.

Final Thought

Clprolf is not about forcing a new way of coding.

It is about providing a clearer structure
that can be applied when needed.

You don’t need to change everything to start.
You can start where you are —
and move toward more structure when it makes sense.

awesome-trending-repos: GitHub Trending için Modern Web Arayüzü

CI/CD Pipeline

Önceki blog yazımda awesome-trending-repos projesinden bahsetmiştim. O zamanlar proje sadece README.md dosyasına veri yazıyordu. Artık things changed. Proje artık tam fonksiyonlu bir modern web uygulaması.

CI/CD Pipeline
GitHub Actions ile otomatik build ve deploy süreci

Neden Web Arayüzü?

README.md dosyası işe yarar ama statiktir. Trending projeleri takip etmek için daha interaktif bir deneyim istedim. Kullanıcıların dil bazlı filtreleme yapabilmesi, arama yapabilmesi ve grafiklerle verileri görebilmesi gerekliydi.

Bu yüzden React + Vite ile modern bir single-page application (SPA) geliştirdim.

Tech Stack

Frontend:

  • React 19 – Son sürüm
  • Vite – Hızlı dev server ve build
  • Tailwind CSS v4 – Utility-first styling
  • Framer Motion – Animasyonlar
  • Recharts – Veri görselleştirme

Backend/Infrastructure:

  • GitHub Actions – Otomasyon
  • GitHub Pages – Hosting
  • Custom Domain – furkankoykiran.com.tr

Proje Yapısı

Projenin temel dizini şöyle:

awesome-trending-repos/
├── src/
│   ├── App.jsx          # Ana React bileşeni
│   ├── main.jsx         # Entry point
│   └── index.css        # Tailwind + custom styles
├── scripts/
│   └── update.js        # Trending veri scrape script
├── public/
│   ├── data/
│   │   └── trending.json # Frontend veri kaynağı
│   └── logo.png
├── .github/workflows/
│   └── update-trends.yml # CI/CD pipeline
└── vite.config.js       # GitHub Pages config

GitHub Pages Entegrasyonu

Vite ile GitHub Pages kullanırken en önemli nokta base path konfigürasyonudur. Projeniz bir subdirectory’de çalışacaksa (örn: github.io/repo-name), vite.config.js dosyasında bunu belirtmelisiniz:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
  base: '/awesome-trending-repos/', // GitHub Pages subdirectory
});

Bu ayar olmadan, production’da asset path’leri bozulur ve 404 hatası alırsınız.

GitHub Actions Workflow

Her gün gece yarısı UTC’de çalışan workflow:

  1. Trending verisi çek – GitHub Trending sayfasını scrape et
  2. Frontend build – npm run build ile dist/ klasörünü oluştur
  3. Data commit – Değişiklik varsa README ve data dosyalarını commit et
  4. Deploy – GitHub Pages’e dağıt
# .github/workflows/update-trends.yml
name: Update and Deploy Trends

on:
  schedule:
    - cron: '0 0 * * *'  # Her gün gece yarısı
  workflow_dispatch:

permissions:
  contents: write
  pages: write
  id-token: write

jobs:
  update-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run update script
        run: npm run update
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Build frontend
        run: npm run build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: './dist'

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: update-and-build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Deployment Flow
Otomatik deployment akışı

Frontend Özellikleri

1. Dil Filtreleme

Kullanıcılar programlama diline göre filtreleme yapabilir:

const languages = ['All', ...new Set(repos.map(r => r.language).filter(Boolean))];

const filteredRepos = repos.filter(repo => {
  const matchesLang = filter === 'All' || repo.language === filter;
  const matchesSearch = repo.name.toLowerCase().includes(searchQuery.toLowerCase());
  return matchesLang && matchesSearch;
});

2. Arama Fonksiyonu

Real-time arama ile repo ismi, owner veya description’da arama yapılır.

3. Görsel İstatistikler

Recharts kütüphanesi ile dil dağılımı bar chart ve growth liderleri listesi:

<ResponsiveContainer width="100%" height="100%">
  <BarChart data={data?.insights?.topLanguages || []}>
    <CartesianGrid strokeDasharray="8 8" stroke="#ffffff03" />
    <XAxis dataKey="language" stroke="#475569" />
    <YAxis stroke="#475569" />
    <Tooltip contentStyle={{ backgroundColor: 'rgba(15, 23, 42, 0.9)' }} />
    <Bar dataKey="count" radius={[12, 12, 0, 0]}>
      {data?.insights?.topLanguages?.map((entry, index) => (
        <Cell key={index} fill={getLangColor(entry.language)} />
      ))}
    </Bar>
  </BarChart>
</ResponsiveContainer>

4. Deployment Feed

Her güncelleme için bir feed entry oluşturulur. Kullanıcılar deployment tarihçesini görebilir.

Custom Domain Ayarı

GitHub Pages için custom domain kullanmak istersen:

  1. CNAME dosyası oluştur – public/CNAME içine domain yaz:
   furkankoykiran.com.tr
  1. DNS ayarı – Domain provider’ında:
   CNAME furkankoykiran.github.io
  1. HTTPS – GitHub Pages otomatik olarak Let’s Encrypt ile SSL sağlar.

Performans İpuçları

1. Asset Optimization

Vite production build otomatik olarak minification ve tree-shaking yapar. Büyük resimler için public/ klasörü yerine optimized image kullanın.

2. Lazy Loading

Büyük listeler için virtual scrolling kullanmayı düşünün. Bu projede 25 repo olduğu için gerekli değil ama scale ederseniz eklenebilir.

3. Caching

GitHub Pages otomatik olarak cache headers ekler. Ancak index.html için cache-busting kullanın:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: `assets/[name].[hash].js`,
        chunkFileNames: `assets/[name].[hash].js`,
        assetFileNames: `assets/[name].[hash].[ext]`
      }
    }
  }
});

Canlı Demo

Proje şu anda furkankoykiran.com.tr/awesome-trending-repos adresinde yayında. Her gün otomatik olarak güncelleniyor.

Project Logo
Proje logosu

OG Image
Open Graph preview görseli

Sonuç

README.md’den modern web uygulamasına geçiş, projenin kullanılabilirliğini önemli ölçüde artırdı. Kullanıcılar artık sadece statik bir liste yerine interaktif bir deneyim yaşıyor.

GitHub Pages + Vite kombinasyonu, static siteler için harika bir çözüm. Ücretsiz, hızlı ve CI/CD ile tam otomatik.

Proje tamamen açık kaynak. Katkı yapmak isterseniz GitHub repo‘ya pull request gönderebilirsiniz.

Kaynak: blog.furkankoykiran.com.tr