We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/homelab-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
# Extending Formatters - Strategy Pattern
## Overview
The formatter layer uses the **Strategy Pattern** to support multiple output formats without modifying handler code. This document explains how to add new output formats.
## Architecture
```
┌─────────────┐
│ Handler │
└──────┬──────┘
│ calls
▼
┌─────────────────┐
│ FormatterFactory│ creates
└──────┬──────────┘
│
▼
┌─────────────────┐
│ IFormatter │ ◄─── Implement this interface
└─────────────────┘
△
│ implements
├────────────────────┐
│ │
┌──────┴────────┐ ┌─────┴────────┐
│ MarkdownFormat│ │ JSONFormatter│
└───────────────┘ └──────────────┘
```
## Current Formatters
### MarkdownFormatter
- **Purpose**: Human-readable documentation format
- **Behavior**:
- String input → returned as-is (assumed to be pre-formatted markdown)
- Object input → wrapped in JSON code block
- **Use case**: Default format for MCP tool responses
### JSONFormatter
- **Purpose**: Machine-readable API responses
- **Behavior**:
- String input → wrapped in `{ output: string, format: "text" }`
- Object input → serialized directly as JSON
- **Use case**: Programmatic consumption, API integration
## Adding a New Format
### Step 1: Implement IFormatter
Create a new formatter class implementing `IFormatter`:
```typescript
// src/formatters/my-formatter.ts
import type { IFormatter } from "./strategy.js"
export class MyFormatter implements IFormatter {
format(data: unknown): string {
if (typeof data === "string") {
// Handle pre-formatted strings from existing formatters
return this.convertMarkdownToMyFormat(data)
}
// Handle structured data
return this.serializeAsMyFormat(data)
}
private convertMarkdownToMyFormat(markdown: string): string {
// Convert markdown to your format
return markdown
}
private serializeAsMyFormat(data: unknown): string {
// Serialize structured data to your format
return String(data)
}
}
```
### Step 2: Update FormatterFactory
Add your format to `FormatterFactory.create()`:
```typescript
// src/formatters/strategy.ts
import { MyFormatter } from "./my-formatter.js"
export class FormatterFactory {
static create(format: "markdown" | "json" | "myformat"): IFormatter {
switch (format) {
case "markdown":
return new MarkdownFormatter()
case "json":
return new JSONFormatter()
case "myformat":
return new MyFormatter()
default: {
const exhaustiveCheck: never = format
throw new Error(`Unsupported format: ${exhaustiveCheck}`)
}
}
}
}
```
### Step 3: Export from Index
Add to barrel export:
```typescript
// src/formatters/index.ts
export { MyFormatter } from "./my-formatter.js"
```
### Step 4: Update Format Type
Add format to the union type where format is validated:
```typescript
// src/schemas/flux/base.ts (or wherever format is defined)
format: z.enum(["markdown", "json", "myformat"]).optional()
```
### Step 5: Write Tests
Create comprehensive tests:
```typescript
// src/formatters/my-formatter.test.ts
import { describe, it, expect } from "vitest"
import { MyFormatter, FormatterFactory } from "./strategy.js"
describe("MyFormatter", () => {
it("should format strings correctly", () => {
const formatter = new MyFormatter()
const result = formatter.format("test input")
expect(result).toBe("expected output")
})
it("should format objects correctly", () => {
const formatter = new MyFormatter()
const result = formatter.format({ key: "value" })
// Assert your format's output
})
})
describe("FormatterFactory integration", () => {
it("should create MyFormatter for 'myformat' format", () => {
const formatter = FormatterFactory.create("myformat")
expect(formatter).toBeInstanceOf(MyFormatter)
})
})
```
## Design Considerations
### Handling Pre-Formatted Strings
Domain-specific formatters (e.g., `formatContainersMarkdown()`) already return formatted strings. Your formatter needs to handle these in two ways:
1. **Pass-through**: If your format is compatible with markdown
```typescript
if (typeof data === "string") return data
```
2. **Conversion**: If you need to transform markdown to your format
```typescript
if (typeof data === "string") {
return this.convertMarkdown(data)
}
```
### Handling Structured Data
When handlers pass structured data (objects, arrays), your formatter should serialize them natively:
```typescript
if (typeof data !== "string") {
return this.serializeToMyFormat(data)
}
```
### Error Handling
Formatters should be defensive:
```typescript
format(data: unknown): string {
try {
// Your formatting logic
return result;
} catch (error) {
// Fallback to safe output
return `Error formatting data: ${error.message}`;
}
}
```
## Handler Integration
Handlers use formatters through `formatResponse()`:
```typescript
// src/tools/handlers/container.ts
import { formatResponse } from "./base-handler.js";
import { formatContainersMarkdown } from "../../formatters/index.js";
export async function handleContainerAction(input, container) {
const { format } = await initializeHandler(input, container, ...);
const containers = await dockerService.listContainers(...);
// formatResponse handles format selection internally
return formatResponse(containers, format, () =>
formatContainersMarkdown(containers, total, offset, hasMore)
);
}
```
The `formatResponse()` utility:
1. Checks the format parameter
2. Creates appropriate formatter via `FormatterFactory`
3. Passes either markdown (from callback) or raw data to formatter
4. Returns formatted string
## Examples
### XML Formatter
```typescript
export class XMLFormatter implements IFormatter {
format(data: unknown): string {
if (typeof data === "string") {
// Wrap markdown text in CDATA
return `<output><![CDATA[${data}]]></output>`
}
return this.toXML(data, "root")
}
private toXML(obj: unknown, tag: string): string {
if (Array.isArray(obj)) {
return obj.map((item) => this.toXML(item, "item")).join("")
}
if (typeof obj === "object" && obj !== null) {
const entries = Object.entries(obj)
const inner = entries.map(([key, value]) => this.toXML(value, key)).join("")
return `<${tag}>${inner}</${tag}>`
}
return `<${tag}>${obj}</${tag}>`
}
}
```
### YAML Formatter
```typescript
import yaml from "js-yaml"
export class YAMLFormatter implements IFormatter {
format(data: unknown): string {
if (typeof data === "string") {
// Wrap markdown as literal block
return `output: |\n${data
.split("\n")
.map((l) => " " + l)
.join("\n")}`
}
return yaml.dump(data, { indent: 2 })
}
}
```
### Table Formatter (ASCII tables)
```typescript
export class TableFormatter implements IFormatter {
format(data: unknown): string {
if (typeof data === "string") {
return data // Pass through markdown tables
}
if (Array.isArray(data) && data.length > 0) {
return this.arrayToTable(data)
}
// Convert object to vertical table
return this.objectToTable(data)
}
private arrayToTable(rows: unknown[]): string {
// Generate ASCII table from array of objects
// ...
}
private objectToTable(obj: unknown): string {
// Generate vertical key-value table
// ...
}
}
```
## Best Practices
1. **Preserve Information**: Don't lose data during formatting
2. **Handle Edge Cases**: Empty data, null values, circular references
3. **Be Deterministic**: Same input → same output (no timestamps, random IDs)
4. **Test Thoroughly**: Unit tests for all data types
5. **Document Behavior**: Explain how strings vs objects are handled
6. **Follow Conventions**: Match existing formatter patterns
7. **Keep It Simple**: Formatters should be pure functions with no side effects
## References
- Strategy Pattern: https://refactoring.guru/design-patterns/strategy
- Implementation: `src/formatters/strategy.ts`
- Tests: `src/formatters/strategy.test.ts`
- Usage: `src/tools/handlers/base-handler.ts` (formatResponse function)