Back to Blog

Cross-Platform AI Skill Development: Write Once, Run Everywhere

Ultrion TeamJuly 22, 202613 min read

title: "Cross-Platform AI Skill Development: Write Once, Run Everywhere" slug: "cross-platform-ai-skill-development" description: "The complete guide to building AI skills that work across MCP, A2A, REST, and multiple agent frameworks β€” architecture patterns, tooling, and deployment strategies." category: "Technical" publishedAt: "2026-07-22"

Cross-Platform AI Skill Development: Write Once, Run Everywhere

AI agents are fragmented across dozens of frameworks, protocols, and platforms. LangChain, CrewAI, AutoGen, Semantic Kernel β€” each has its own tool format. MCP, A2A, and REST APIs each expect different communication patterns. For skill creators, this fragmentation means building the same skill 3-5 times to reach all platforms. This guide shows you how to build once and deploy everywhere.

The Fragmentation Problem

Current Landscape

Platform/Framework Tool Format Protocol Language
Claude (Anthropic) MCP JSON-RPC TypeScript/Python
ChatGPT (OpenAI) Function Calling HTTP REST Any
LangChain Tools API Python/JS Python/TypeScript
CrewAI Tools Python Python
AutoGen Function Calling Python Python
Semantic Kernel Plugins HTTP/gRPC C#/Python
SkillExchange MCP Skills Streamable HTTP Any
Smithery MCP HTTP Any

Building a skill for all of these means maintaining 5+ codebases. That's unsustainable for individual developers.

The Solution: Protocol-First Skill Architecture

The key insight: separate your skill's logic from its delivery mechanism. Build the core skill once, then expose it through multiple protocol adapters.

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚    Skill Core (Logic)    β”‚
                    β”‚                         β”‚
                    β”‚  β€’ Business logic       β”‚
                    β”‚  β€’ Input processing      β”‚
                    β”‚  β€’ Output formatting     β”‚
                    β”‚  β€’ Error handling        β”‚
                    β”‚  β€’ Validation            β”‚
                    β”‚                         β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Protocol Adapter Layer β”‚
                    β”‚                         β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚         β”‚         β”‚         β”‚                β”‚
    β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
    β”‚  MCP   β”‚ β”‚  A2A  β”‚ β”‚ REST  β”‚ β”‚  Lang  β”‚ β”‚   Native    β”‚
    β”‚Adapter β”‚ β”‚Adapterβ”‚ β”‚Adapterβ”‚ β”‚ Chain  β”‚ β”‚   SDK       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Building the Skill Core

Your skill core should be a pure function that takes input and returns output:

// skill-core.ts β€” The single source of truth
export interface SkillInput {
  document: string;
  options?: {
    language?: string;
    detail_level?: "summary" | "detailed" | "comprehensive";
    format?: "text" | "json" | "markdown";
  };
}

export interface SkillOutput {
  result: string;
  metadata: {
    processing_time_ms: number;
    confidence: number;
    model_used: string;
    tokens_consumed: number;
  };
}

export class DocumentAnalyzerSkill {
  readonly name = "document-analyzer";
  readonly version = "1.2.0";
  readonly description = "Analyzes documents and extracts key insights, summaries, and actionable items.";
  
  // Zod schema for validation (used by all adapters)
  readonly inputSchema = z.object({
    document: z.string().min(1).max(500000),
    options: z.object({
      language: z.string().default("en"),
      detail_level: z.enum(["summary", "detailed", "comprehensive"]).default("detailed"),
      format: z.enum(["text", "json", "markdown"]).default("markdown")
    }).optional().default({})
  });
  
  async execute(input: SkillInput): Promise<SkillOutput> {
    const start = Date.now();
    
    // 1. Validate input
    const validated = this.inputSchema.parse(input);
    
    // 2. Process based on detail level
    const model = this.selectModel(validated.options.detail_level);
    const analysis = await this.analyze(validated, model);
    
    // 3. Format output
    const formatted = this.formatOutput(analysis, validated.options.format);
    
    return {
      result: formatted,
      metadata: {
        processing_time_ms: Date.now() - start,
        confidence: analysis.confidence,
        model_used: model,
        tokens_consumed: analysis.tokens
      }
    };
  }
  
  private selectModel(detailLevel: string): string {
    const modelMap = {
      "summary": "glm-4.7-flash",      // Fast, cheap
      "detailed": "glm-5.0",           // Balanced
      "comprehensive": "glm-5.1"        // Best quality
    };
    return modelMap[detailLevel] || "glm-5.0";
  }
  
  private async analyze(input: SkillInput, model: string) {
    // Core analysis logic β€” model-agnostic
    const prompt = this.buildPrompt(input);
    const response = await llmClient.complete(model, prompt);
    return this.parseAnalysis(response);
  }
}

Protocol Adapters

MCP Adapter

// adapters/mcp.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { DocumentAnalyzerSkill } from "./skill-core";

const skill = new DocumentAnalyzerSkill();
const server = new McpServer({
  name: skill.name,
  version: skill.version
});

// Register as MCP tool
server.tool(
  skill.name,
  skill.description,
  skill.inputSchema,
  async (params) => {
    const result = await skill.execute(params);
    return {
      content: [{
        type: "text",
        text: result.result
      }],
      metadata: result.metadata
    };
  }
);

// Start MCP server
const transport = new StreamableHTTPServerTransport({});
await server.connect(transport);

const app = express();
app.post("/mcp", transport.handleRequest);
app.listen(3000);

REST API Adapter

// adapters/rest.ts
import express from "express";
import { DocumentAnalyzerSkill } from "./skill-core";

const skill = new DocumentAnalyzerSkill();
const app = express();

app.post("/analyze", express.json({ limit: "50mb" }), async (req, res) => {
  try {
    const result = await skill.execute(req.body);
    res.json({
      result: result.result,
      metadata: result.metadata
    });
  } catch (error) {
    if (error instanceof z.ZodError) {
      res.status(400).json({ error: "Invalid input", details: error.errors });
    } else {
      res.status(500).json({ error: "Analysis failed", message: error.message });
    }
  }
});

// Health check
app.get("/health", (req, res) => res.json({ status: "ok", version: skill.version }));

// OpenAPI spec (auto-generated)
app.get("/openapi.json", (req, res) => {
  res.json({
    openapi: "3.1.0",
    info: { title: skill.name, version: skill.version },
    paths: {
      "/analyze": {
        post: {
          summary: skill.description,
          requestBody: { required: true, content: { "application/json": { schema: skill.inputSchema } } },
          responses: { "200": { description: "Success" } }
        }
      }
    }
  });
});

app.listen(8080);

LangChain Tool Adapter

// adapters/langchain.ts
import { DynamicTool } from "@langchain/core/tools";
import { DocumentAnalyzerSkill } from "./skill-core";

const skill = new DocumentAnalyzerSkill();

export const documentAnalyzerTool = new DynamicTool({
  name: skill.name,
  description: skill.description,
  func: async (input: string) => {
    // LangChain passes input as a string β€” parse it
    const params = typeof input === "string" ? JSON.parse(input) : input;
    const result = await skill.execute(params);
    return result.result;
  }
});

// For LangChain.js structured tools
import { StructuredTool } from "@langchain/core/tools";
import { z } from "zod";

export class DocumentAnalyzerStructuredTool extends StructuredTool {
  name = skill.name;
  description = skill.description;
  schema = skill.inputSchema;
  
  async _call(input: z.infer<typeof skill.inputSchema>): Promise<string> {
    const result = await skill.execute(input);
    return result.result;
  }
}

CrewAI Tool Adapter

# adapters/crewai.py
from crewai_tools import BaseTool
from pydantic import BaseModel, Field
import httpx

class DocumentAnalyzerInput(BaseModel):
    document: str = Field(..., description="The document text to analyze")
    detail_level: str = Field("detailed", description="Level of detail: summary, detailed, comprehensive")

class DocumentAnalyzerTool(BaseTool):
    name: str = "document_analyzer"
    description: str = "Analyzes documents and extracts key insights, summaries, and actionable items."
    args_schema: type[BaseModel] = DocumentAnalyzerInput
    
    def _run(self, **kwargs) -> str:
        # Call the skill core via REST API
        response = httpx.post(
            "http://skill-core:8080/analyze",
            json=kwargs,
            timeout=30.0
        )
        response.raise_for_status()
        return response.json()["result"]

OpenAI Function Calling Adapter

// adapters/openai.ts
import { DocumentAnalyzerSkill } from "./skill-core";

const skill = new DocumentAnalyzerSkill();

// Export as OpenAI function definition
export const functionDefinition = {
  type: "function",
  function: {
    name: skill.name,
    description: skill.description,
    parameters: {
      type: "object",
      properties: {
        document: { type: "string", description: "The document text to analyze" },
        options: {
          type: "object",
          properties: {
            language: { type: "string", default: "en" },
            detail_level: { type: "string", enum: ["summary", "detailed", "comprehensive"] },
            format: { type: "string", enum: ["text", "json", "markdown"] }
          }
        }
      },
      required: ["document"]
    }
  }
};

// Handler for OpenAI function calls
export async function handleFunctionCall(args: string): Promise<string> {
  const input = JSON.parse(args);
  const result = await skill.execute(input);
  return result.result;
}

Build and Deployment Strategy

Build Configuration

// package.json
{
  "name": "document-analyzer-skill",
  "scripts": {
    "build:all": "npm run build:mcp && npm run build:rest && npm run build:langchain",
    "build:mcp": "esbuild src/adapters/mcp.ts --bundle --platform=node --outfile=dist/mcp.js",
    "build:rest": "esbuild src/adapters/rest.ts --bundle --platform=node --outfile=dist/rest.js",
    "build:langchain": "tsc --project tsconfig.langchain.json",
    "build:crewai": "echo 'Python adapter β€” build separately'",
    "package": "node scripts/package-all.js",
    "publish:skillexchange": "node scripts/publish-to-skillexchange.js",
    "publish:npm": "npm publish",
    "publish:pypi": "cd adapters/crewai && python -m build && twine upload dist/*"
  }
}

Docker Setup for Multi-Protocol Deployment

FROM node:22-slim AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ ./dist/

# MCP Server image
FROM base AS mcp-server
EXPOSE 3000
CMD ["node", "dist/mcp.js"]

# REST API image
FROM base AS rest-api
EXPOSE 8080
CMD ["node", "dist/rest.js"]

Docker Compose for All Adapters

version: "3.9"
services:
  mcp-server:
    build:
      context: .
      target: mcp-server
    ports: ["3000:3000"]
    
  rest-api:
    build:
      context: .
      target: rest-api
    ports: ["8080:8080"]
    
  # The REST API is also accessible for CrewAI, AutoGen, etc.

SDK Generation

Auto-generate SDKs for popular languages from your input schema:

// scripts/generate-sdks.ts
import { zodToTypeScript, zodToPython } from "@skill/sdk-generator";

const skill = new DocumentAnalyzerSkill();

// Generate TypeScript SDK
const tsSDK = generateTypeScriptSDK({
  name: skill.name,
  schema: skill.inputSchema,
  endpoint: "https://my-skill.com/api"
});

// Generate Python SDK
const pySDK = generatePythonSDK({
  name: skill.name,
  schema: skill.inputSchema,
  endpoint: "https://my-skill.com/api"
});

// Generate OpenAPI spec (for auto-client generation in any language)
const openApiSpec = generateOpenAPI({
  name: skill.name,
  description: skill.description,
  schema: skill.inputSchema
});

Publishing to Multiple Marketplaces

// scripts/publish-all.ts
import { SkillExchangeClient, SmitheryClient, NPMClient } from "@skill/publishers";

const skill = new DocumentAnalyzerSkill();

// Publish to SkillExchange
await new SkillExchangeClient().publish({
  name: skill.name,
  description: skill.description,
  endpoint: "https://my-skill.com/mcp",
  protocol: "MCP",
  pricing: { type: "per_invocation", price: 0.03, currency: "EUR" }
});

// Publish to Smithery
await new SmitheryClient().publish({
  name: skill.name,
  sourceUrl: "https://github.com/me/document-analyzer",
  mcpConfig: "./mcp.json"
});

// Publish to NPM (for LangChain/Direct usage)
await new NPMClient().publish({
  name: `@me/${skill.name}`,
  version: skill.version,
  entryPoint: "./dist/langchain.js"
});

Testing Across Platforms

// tests/cross-platform.test.ts
import { DocumentAnalyzerSkill } from "../src/skill-core";

const TEST_INPUTS = [
  { document: "Short text", options: { detail_level: "summary" } },
  { document: "Medium length document with more context...", options: { detail_level: "detailed" } },
  { document: "Very long document...", options: { detail_level: "comprehensive" } },
];

describe("Cross-platform consistency", () => {
  const skill = new DocumentAnalyzerSkill();
  
  for (const input of TEST_INPUTS) {
    test(`produces consistent output for: ${input.document.slice(0, 20)}...`, async () => {
      // Test through skill core
      const coreResult = await skill.execute(input);
      
      // Test through REST API
      const restResult = await fetch("http://localhost:8080/analyze", {
        method: "POST",
        body: JSON.stringify(input)
      }).then(r => r.json());
      
      // Test through MCP
      const mcpResult = await mcpClient.callTool(skill.name, input);
      
      // Outputs should be equivalent (not identical due to model non-determinism)
      expect(restResult.result).toBeTruthy();
      expect(mcpResult.content[0].text).toBeTruthy();
      
      // Quality should be similar
      const coreScore = await qualityScorer.score(coreResult.result, input);
      const restScore = await qualityScorer.score(restResult.result, input);
      expect(Math.abs(coreScore - restScore)).toBeLessThan(0.1);
    });
  }
});

Conclusion

Cross-platform AI skill development isn't about writing everything once β€” it's about separating concerns correctly. Build your skill core as a pure, protocol-agnostic module. Then add thin adapters for each platform. This approach reduces maintenance by 70%, ensures consistency across platforms, and lets you reach every agent ecosystem from a single codebase.

The future of AI skills is protocol-agnostic β€” build for that future today. Your skill core should outlast every framework, protocol, and platform that currently exists.

Newsletter

Enjoying this article?

Get weekly insights on building and selling AI skills, MCP tools, and creator economics. Join 2,000+ AI builders and creators.

No spam. Unsubscribe anytime.

Get the Free MCP Server Handbook

50+ pages of practical guides, code examples, and production-ready templates.

  • Complete MCP protocol reference
  • 15+ production-ready templates
  • Security best practices guide

No spam. Unsubscribe anytime. We respect your privacy.

Related Articles

Ready to try AI skills?

Browse the marketplace and discover skills for your AI agents.

Browse Skills