Skip to main content
Glama

template_card

Transform a static Adaptive Card into a template with data binding expressions. Accepts optional data shape or card description for template creation.

Instructions

Convert a static Adaptive Card into an Adaptive Card Template with ${expression} data binding.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
cardNoA static Adaptive Card or cardId to convert into a template
dataShapeNoOptional data shape hint
descriptionNoIf no card is provided, describe the card to generate as a template

Implementation Reference

  • Main handler function for the template_card tool. Accepts a static Adaptive Card (or a description to generate one), walks the card tree to replace dynamic values with template expressions (${...}), and returns the templated card, sample data, expressions list, and a binding guide.
    export function handleTemplateCard(input: TemplateCardInput): TemplateCardOutput {
      const { card, dataShape, description } = input;
    
      let sourceCard: Record<string, unknown>;
    
      if (card) {
        sourceCard = JSON.parse(JSON.stringify(card));
      } else if (description) {
        // Generate a card from the description, then templatize it
        sourceCard = assembleCard({
          content: description,
          version: "1.6",
        });
      } else {
        throw new Error("Either 'card' or 'description' must be provided");
      }
    
      const expressions: ExpressionEntry[] = [];
      const sampleData: Record<string, unknown> = {};
      const repeatedDataSamples: Record<string, unknown[]> = {};
    
      // Templatize the card
      const template = templatizeNode(sourceCard, "$", expressions, sampleData, repeatedDataSamples, dataShape);
    
      // Merge repeated data samples into sampleData
      for (const [key, samples] of Object.entries(repeatedDataSamples)) {
        sampleData[key] = samples;
      }
    
      // Build binding guide
      const bindingGuide = buildBindingGuide(expressions, sampleData);
    
      return {
        template: template as Record<string, unknown>,
        sampleData,
        expressions,
        bindingGuide,
      };
    }
  • Input type definition for template_card: accepts an optional card JSON, optional dataShape hint, and optional description (to generate a card from scratch).
    export interface TemplateCardInput {
      card?: Record<string, unknown>;
      dataShape?: Record<string, unknown>;
      description?: string;
    }
  • Output type definition for template_card: returns the template JSON, sampleData, expressions array (path/expression/description), and a bindingGuide string.
    export interface TemplateCardOutput {
      template: Record<string, unknown>;
      sampleData: Record<string, unknown>;
      expressions: Array<{
        path: string;
        expression: string;
        description: string;
      }>;
      bindingGuide: string;
    }
  • Tool registration in the MCP server: defines name 'template_card', description, and inputSchema with optional card, dataShape, and description properties.
    {
      name: "template_card",
      description:
        "Convert a static Adaptive Card into an Adaptive Card Template with ${expression} data binding.",
      inputSchema: {
        type: "object" as const,
        properties: {
          card: {
            description: "A static Adaptive Card or cardId to convert into a template",
          },
          dataShape: {
            type: "object",
            description: "Optional data shape hint",
          },
          description: {
            type: "string",
            description: "If no card is provided, describe the card to generate as a template",
          },
        },
      },
    },
  • Case statement in the tool dispatch handler that routes 'template_card' calls to handleTemplateCard().
    case "template_card": {
      result = handleTemplateCard(parsed as any);
      break;
    }
  • Core helper functions: templatizeNode (recursive walk), templatizeObject (handles TextBlock/Image/Action/FactSet properties), templatizeArray (detects repeated structures with $data binding), templatizeString (string value detection), plus detection/naming helpers and buildBindingGuide.
    function templatizeNode(
      node: unknown,
      path: string,
      expressions: ExpressionEntry[],
      sampleData: Record<string, unknown>,
      repeatedDataSamples: Record<string, unknown[]>,
      dataShape?: Record<string, unknown>,
    ): unknown {
      if (node === null || node === undefined) return node;
    
      if (typeof node === "string") {
        return templatizeString(node, path, expressions, sampleData, dataShape);
      }
    
      if (typeof node === "number" || typeof node === "boolean") {
        return node;
      }
    
      if (Array.isArray(node)) {
        return templatizeArray(node, path, expressions, sampleData, repeatedDataSamples, dataShape);
      }
    
      if (typeof node === "object") {
        return templatizeObject(
          node as Record<string, unknown>,
          path,
          expressions,
          sampleData,
          repeatedDataSamples,
          dataShape,
        );
      }
    
      return node;
    }
    
    function templatizeObject(
      obj: Record<string, unknown>,
      path: string,
      expressions: ExpressionEntry[],
      sampleData: Record<string, unknown>,
      repeatedDataSamples: Record<string, unknown[]>,
      dataShape?: Record<string, unknown>,
    ): Record<string, unknown> {
      const result: Record<string, unknown> = {};
      const type = obj.type as string | undefined;
    
      for (const [key, value] of Object.entries(obj)) {
        const childPath = `${path}.${key}`;
    
        // Skip schema/meta properties
        if (key === "type" || key === "$schema") {
          result[key] = value;
          continue;
        }
    
        // Skip version at card root
        if (key === "version" && path === "$") {
          result[key] = value;
          continue;
        }
    
        // Handle specific element properties that should be templatized
        if (type === "TextBlock" && key === "text" && typeof value === "string") {
          const propName = inferPropertyName(value, path, "text");
          if (!value.includes("${")) {
            result[key] = `\${${propName}}`;
            sampleData[propName] = value;
            expressions.push({
              path: childPath,
              expression: `\${${propName}}`,
              description: `Text content for TextBlock at ${path}`,
            });
          } else {
            result[key] = value;
          }
          continue;
        }
    
        if (type === "Image" && key === "url" && typeof value === "string") {
          const propName = inferPropertyName(value, path, "imageUrl");
          if (!value.includes("${")) {
            result[key] = `\${${propName}}`;
            sampleData[propName] = value;
            expressions.push({
              path: childPath,
              expression: `\${${propName}}`,
              description: `Image URL at ${path}`,
            });
          } else {
            result[key] = value;
          }
          continue;
        }
    
        if (type === "Image" && key === "altText" && typeof value === "string") {
          const propName = inferPropertyName(value, path, "imageAlt");
          if (!value.includes("${")) {
            result[key] = `\${${propName}}`;
            sampleData[propName] = value;
            expressions.push({
              path: childPath,
              expression: `\${${propName}}`,
              description: `Alt text for Image at ${path}`,
            });
          } else {
            result[key] = value;
          }
          continue;
        }
    
        if (key === "url" && type?.startsWith("Action.") && typeof value === "string") {
          const propName = inferPropertyName(value, path, "actionUrl");
          if (!value.includes("${")) {
            result[key] = `\${${propName}}`;
            sampleData[propName] = value;
            expressions.push({
              path: childPath,
              expression: `\${${propName}}`,
              description: `Action URL at ${path}`,
            });
          } else {
            result[key] = value;
          }
          continue;
        }
    
        // Handle FactSet facts specially — templatize individual fact values
        if (type === "FactSet" && key === "facts" && Array.isArray(value)) {
          result[key] = value.map((fact, idx) => {
            const f = fact as Record<string, unknown>;
            const factTitle = String(f.title || "");
            const factValue = String(f.value || "");
            const titleProp = camelCase(factTitle || `factTitle${idx}`);
            const valueProp = camelCase(factTitle ? `${factTitle}Value` : `factValue${idx}`);
    
            if (factValue && !factValue.includes("${")) {
              sampleData[valueProp] = factValue;
              expressions.push({
                path: `${childPath}[${idx}].value`,
                expression: `\${${valueProp}}`,
                description: `Value for fact "${factTitle}"`,
              });
            }
    
            return {
              title: factTitle,
              value: factValue.includes("${") ? factValue : `\${${valueProp}}`,
            };
          });
          continue;
        }
    
        // Recurse into nested structures
        result[key] = templatizeNode(value, childPath, expressions, sampleData, repeatedDataSamples, dataShape);
      }
    
      return result;
    }
    
    function templatizeArray(
      arr: unknown[],
      path: string,
      expressions: ExpressionEntry[],
      sampleData: Record<string, unknown>,
      repeatedDataSamples: Record<string, unknown[]>,
      dataShape?: Record<string, unknown>,
    ): unknown[] {
      // Detect repeated structures (e.g., Table rows, list items)
      if (arr.length >= 2 && areStructurallyHomogeneous(arr)) {
        const dataKey = inferRepeatedDataKey(path);
    
        // Build a template from the first item with $data binding
        const templateItem = templatizeRepeatedItem(
          arr[0] as Record<string, unknown>,
          `${path}[0]`,
          expressions,
          dataKey,
        );
    
        // Generate sample data from all items
        const samples = arr.map((item) =>
          extractSampleFromRepeatedItem(item as Record<string, unknown>),
        );
        repeatedDataSamples[dataKey] = samples;
    
        // Add $data binding to the template item
        if (typeof templateItem === "object" && templateItem !== null) {
          (templateItem as Record<string, unknown>)["$data"] = `\${${dataKey}}`;
          expressions.push({
            path: `${path}[0].$data`,
            expression: `\${${dataKey}}`,
            description: `Data binding for repeated items in ${path}. Each item in the ${dataKey} array will generate one instance.`,
          });
        }
    
        return [templateItem];
      }
    
      // Non-repeated arrays — recurse into each element
      return arr.map((item, i) =>
        templatizeNode(item, `${path}[${i}]`, expressions, sampleData, repeatedDataSamples, dataShape),
      );
    }
    
    function templatizeString(
      value: string,
      path: string,
      expressions: ExpressionEntry[],
      sampleData: Record<string, unknown>,
      dataShape?: Record<string, unknown>,
    ): string {
      // Already a template expression
      if (value.includes("${")) return value;
    
      // Don't templatize certain static values
      if (isStaticValue(value)) return value;
    
      // Check if this looks like a dynamic value
      if (looksLikeDynamicValue(value, path)) {
        const propName = inferPropertyName(value, path, "value");
        sampleData[propName] = value;
        expressions.push({
          path,
          expression: `\${${propName}}`,
          description: `Dynamic value at ${path}`,
        });
        return `\${${propName}}`;
      }
    
      return value;
    }
    
    // ─── Repeated Item Handling ──────────────────────────────────────────────────
    
    function templatizeRepeatedItem(
      item: Record<string, unknown>,
      path: string,
      expressions: ExpressionEntry[],
      dataKey: string,
    ): Record<string, unknown> {
      const result: Record<string, unknown> = {};
    
      for (const [key, value] of Object.entries(item)) {
        if (key === "type") {
          result[key] = value;
          continue;
        }
    
        if (typeof value === "string" && !value.includes("${") && !isStaticValue(value)) {
          const propName = camelCase(key);
          result[key] = `\${${propName}}`;
          expressions.push({
            path: `${path}.${key}`,
            expression: `\${${propName}}`,
            description: `${key} field within each ${dataKey} item`,
          });
        } else if (Array.isArray(value)) {
          // Recurse into nested arrays (e.g., Table cells)
          result[key] = value.map((child, i) => {
            if (child && typeof child === "object") {
              return templatizeRepeatedItem(
                child as Record<string, unknown>,
                `${path}.${key}[${i}]`,
                expressions,
                dataKey,
              );
            }
            return child;
          });
        } else if (value && typeof value === "object") {
          result[key] = templatizeRepeatedItem(
            value as Record<string, unknown>,
            `${path}.${key}`,
            expressions,
            dataKey,
          );
        } else {
          result[key] = value;
        }
      }
    
      return result;
    }
    
    function extractSampleFromRepeatedItem(item: Record<string, unknown>): Record<string, unknown> {
      const sample: Record<string, unknown> = {};
    
      for (const [key, value] of Object.entries(item)) {
        if (key === "type") continue;
    
        if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
          sample[camelCase(key)] = value;
        } else if (Array.isArray(value)) {
          // Look for text values in nested cell structures
          for (const child of value) {
            if (child && typeof child === "object") {
              const nested = extractSampleFromRepeatedItem(child as Record<string, unknown>);
              Object.assign(sample, nested);
            }
          }
        } else if (value && typeof value === "object") {
          const nested = extractSampleFromRepeatedItem(value as Record<string, unknown>);
          Object.assign(sample, nested);
        }
      }
    
      return sample;
    }
    
    // ─── Detection Helpers ───────────────────────────────────────────────────────
    
    function areStructurallyHomogeneous(arr: unknown[]): boolean {
      if (arr.length < 2) return false;
    
      // All items must be objects with the same type
      const types = arr.map((item) => {
        if (!item || typeof item !== "object") return null;
        return (item as Record<string, unknown>).type as string | undefined;
      });
    
      const firstType = types[0];
      if (!firstType) return false;
    
      return types.every((t) => t === firstType);
    }
    
    function isStaticValue(value: string): boolean {
      // Don't templatize adaptive card schema values
      const staticPatterns = [
        /^AdaptiveCard$/,
        /^Column$/,
        /^TableRow$/,
        /^TableCell$/,
        /^CarouselPage$/,
        /^Action\./,
        /^Input\./,
        /^\d+\.\d+$/,         // version strings
        /^(auto|stretch)$/,
        /^(none|small|default|medium|large|extraLarge|padding)$/, // spacing
        /^(left|center|right)$/,
        /^(top|bottom)$/,
        /^(lighter|bolder)$/,
        /^(dark|light|accent|good|warning|attention)$/,
        /^(default|positive|destructive)$/,
        /^(primary|secondary)$/,
        /^(compact|expanded|filtered)$/,
        /^(emphasis|good|attention|warning|accent)$/,
        /^(person|default)$/,       // image style
        /^(heading|columnHeader)$/, // text style
      ];
    
      return staticPatterns.some((p) => p.test(value));
    }
    
    function looksLikeDynamicValue(value: string, path: string): boolean {
      // URLs are dynamic
      if (/^https?:\/\//.test(value)) return true;
    
      // Dates
      if (/^\d{4}-\d{2}-\d{2}/.test(value)) return true;
    
      // Email-like
      if (/\S+@\S+\.\S+/.test(value)) return true;
    
      // Numbers as strings
      if (/^[\d,.]+%?$/.test(value) && value.length <= 20) return true;
    
      // Longer text (likely content, not a label)
      if (value.length > 30) return true;
    
      // Path contains known dynamic property names
      const dynamicPathParts = [".text", ".value", ".url", ".altText", ".title"];
      if (dynamicPathParts.some((p) => path.endsWith(p))) return true;
    
      return false;
    }
    
    // ─── Naming Helpers ──────────────────────────────────────────────────────────
    
    function inferPropertyName(value: string, path: string, fallback: string): string {
      // Try to derive a meaningful name from the path
      const pathParts = path.split(".");
      const lastPart = pathParts[pathParts.length - 1];
    
      // Check for known property names
      if (lastPart === "text") {
        // Try to use parent context
        const parentIndex = pathParts[pathParts.length - 2];
        if (parentIndex && parentIndex.includes("[0]")) return "title";
        if (parentIndex && parentIndex.includes("[1]")) return "subtitle";
    
        // Derive from value content
        if (value.length <= 30) {
          return camelCase(value.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/).slice(0, 3).join(" "));
        }
        return fallback;
      }
    
      if (lastPart === "url") {
        if (path.includes("Image")) return "imageUrl";
        if (path.includes("Action")) return "actionUrl";
        return "url";
      }
    
      if (lastPart === "altText") return "imageAltText";
    
      // Use the path leaf as property name
      const cleaned = lastPart.replace(/\[\d+\]/g, "");
      return camelCase(cleaned) || fallback;
    }
    
    function inferRepeatedDataKey(path: string): string {
      // Derive array data key from path context
      if (path.includes("rows")) return "rows";
      if (path.includes("facts")) return "facts";
      if (path.includes("images")) return "images";
      if (path.includes("columns")) return "columns";
      if (path.includes("pages")) return "pages";
      if (path.includes("actions")) return "actions";
      if (path.includes("items")) return "items";
      return "items";
    }
    
    function camelCase(str: string): string {
      if (!str) return "value";
      return str
        .replace(/[^a-zA-Z0-9\s]/g, "")
        .trim()
        .split(/\s+/)
        .map((word, i) =>
          i === 0
            ? word.toLowerCase()
            : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
        )
        .join("") || "value";
    }
  • Public API export: re-exports handleTemplateCard as templateCard for programmatic use.
    export { handleTemplateCard as templateCard } from "./tools/template-card.js";
    export { handleTransformCard as transformCard } from "./tools/transform-card.js";
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden. It states the conversion to template with data binding, but does not disclose side effects, idempotency, or what happens to the original card. Adequate but minimal.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One concise sentence that front-loads the core transformation. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema is provided, and the description does not explain the return format or how the template is structured. With three parameters and a transformation, more detail is needed for complete understanding.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds marginal value: it repeats what the schema says about 'card' and 'description', and provides no extra semantic detail for 'dataShape'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb 'convert', the resource 'static Adaptive Card', and the outcome 'Adaptive Card Template with ${expression} data binding'. It uniquely identifies the tool's purpose among siblings like generate_card and validate_card.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool versus alternatives such as generate_card or transform_card. No when-not-to-use conditions or prerequisites mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/VikrantSingh01/adaptive-cards-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server