@backstack-io/example
v1.0.0
Published
Reference implementation demonstrating MCP YAML bridge patterns with mathematical functions
Downloads
6
Maintainers
Readme
@backstack/example
Reference implementation for creating MCP tools with the YAML bridge
A production-quality example package demonstrating how to create custom MCP (Model Context Protocol) tools using the @backstack/mcp-yaml-bridge. This package showcases best practices for function implementation, YAML configuration, testing, and documentation.
📖 Table of Contents
- What is This?
- Quick Start
- Available Tools
- Creating Your Own Package
- Best Practices
- Architecture Patterns
- Advanced Topics
- Troubleshooting
- API Reference
- Contributing
- License
What is This?
This package demonstrates how to create MCP tools that can be used by AI agents in the Backstack platform. Instead of implementing the full MCP protocol, you simply:
- Write standard JavaScript/TypeScript functions
- Define them in a
mcp-tools.yamlfile - Publish to NPM
The @backstack/mcp-yaml-bridge handles all the MCP protocol details for you.
Who Should Use This?
- Developers building custom tools for Backstack workspaces
- Team leads wanting to extend AI agent capabilities
- Anyone looking to understand MCP tool development patterns
What You'll Learn
- How to structure an MCP tool package
- Function implementation patterns with async/await
- JSON Schema design for input validation
- Environment variable management
- Testing strategies for MCP tools
- Documentation best practices
Quick Start
Installation
npm install @backstack/exampleTesting Locally
You can test this package locally with the MCP YAML bridge:
# Install the bridge globally
npm install -g @backstack/mcp-yaml-bridge
# Run the package
mcp-yaml-bridge @backstack/exampleThe bridge will start a JSON-RPC server on stdin/stdout that implements the MCP protocol.
Example Usage
// Import a function directly (for testing)
const { addNumbers } = require('@backstack/example/src/math/basic-math');
async function demo() {
const result = await addNumbers({ a: 5, b: 3 });
console.log(result);
// {
// operation: 'addition',
// operands: [5, 3],
// result: 8,
// timestamp: '2024-01-14T12:00:00.000Z'
// }
}
demo();Using with MCP Bridge
# Send a tools/list request
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | mcp-yaml-bridge @backstack/example
# Call add_numbers tool
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add_numbers","arguments":{"a":5,"b":3}}}' | mcp-yaml-bridge @backstack/exampleAvailable Tools
This package includes four mathematical tools demonstrating different patterns:
1. add_numbers
Purpose: Demonstrates the simplest possible MCP tool implementation.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| a | number | Yes | The first number to add |
| b | number | Yes | The second number to add |
Example:
const result = await addNumbers({ a: 5, b: 3 });Output:
{
"operation": "addition",
"operands": [5, 3],
"result": 8,
"timestamp": "2024-01-14T12:00:00.000Z"
}Common Errors:
- Missing parameters → Schema validation error
- Non-numeric values → Schema validation error
2. multiply_numbers
Purpose: Demonstrates default values and enum constraints.
Parameters:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| a | number | Yes | - | The first number to multiply |
| b | number | Yes | - | The second number to multiply |
| precision | integer | No | 2 | Decimal places for rounding (0-5) |
Example:
const result = await multiplyNumbers({ a: 1.234, b: 2, precision: 3 });Output:
{
"operation": "multiplication",
"operands": [1.234, 2],
"result": 2.468,
"precision": 3,
"timestamp": "2024-01-14T12:00:00.000Z"
}Common Errors:
precisionoutside 0-5 range → Schema validation error- Invalid precision type → Schema validation error
3. calculate_statistics
Purpose: Demonstrates array input handling and error validation.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| numbers | array | Yes | Array of numbers to analyze (1-1000 items) |
Example:
const result = await calculateStatistics({ numbers: [1, 2, 3, 4, 5] });Output:
{
"count": 5,
"sum": 15,
"mean": 3,
"median": 3,
"min": 1,
"max": 5,
"timestamp": "2024-01-14T12:00:00.000Z"
}Common Errors:
- Empty array → Runtime error: "Array must contain at least one number"
- Array too large (>1000) → Schema validation error
- Non-numeric array items → Schema validation error
4. format_currency
Purpose: Demonstrates environment variable usage and complex defaults.
Parameters:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| amount | number | Yes | - | The numeric amount to format |
| currency | string | No | USD | ISO 4217 code (USD, EUR, GBP, JPY, CAD, AUD) |
| locale | string | No | en-US | Locale for formatting (en-US, en-GB, de-DE, fr-FR, ja-JP) |
Environment Variables:
CURRENCY_API_KEY(optional): Enables exchange rate conversion
Example:
const result = await formatCurrency({
amount: 100,
currency: 'EUR',
locale: 'de-DE'
});Output:
{
"originalAmount": 100,
"currency": "EUR",
"locale": "de-DE",
"formatted": "85,00 €",
"exchangeRate": 0.85,
"apiKeyUsed": true,
"timestamp": "2024-01-14T12:00:00.000Z"
}Common Errors:
- Invalid currency code → Schema validation error
- Invalid locale → Schema validation error
- Missing CURRENCY_API_KEY when expected → Function works but uses exchangeRate of 1.0
Creating Your Own Package
Follow these steps to create your own MCP tool package based on this example:
Step 1: Initialize Your NPM Package
# Create a new directory
mkdir my-mcp-tools
cd my-mcp-tools
# Initialize package.json
npm init -y
# Update package.json with proper metadata
npm pkg set name="@myorg/my-tools"
npm pkg set description="My custom MCP tools"
npm pkg set version="1.0.0"
npm pkg set license="MIT"
# Create directory structure
mkdir -p src testsStep 2: Create mcp-tools.yaml
Create a mcp-tools.yaml file in your package root:
version: "1.0"
name: "@myorg/my-tools"
description: "Description of your tools"
tools:
- name: my_tool_name
description: "What your tool does (10+ chars)"
implementation: ./src/my-function.js
function: myFunction
inputSchema:
type: object
properties:
param1:
type: string
description: "Parameter description"
required:
- param1
additionalProperties: falseKey points:
- Tool names use
snake_case - Function names use
camelCase - Descriptions must be 10+ characters
- Always set
additionalProperties: false
Step 3: Implement Your Functions
Create your implementation file (e.g., src/my-function.js):
/**
* Brief description of what the function does.
*
* @param {Object} args - Function arguments
* @param {string} args.param1 - Description
* @returns {Promise<Object>} Result object
*/
async function myFunction(args) {
const { param1 } = args;
// Your implementation here
return {
result: 'your result',
timestamp: new Date().toISOString()
};
}
module.exports = { myFunction };Key points:
- Always use async functions for consistency
- Destructure the args object
- Return plain objects (not classes or special types)
- Include a timestamp for debugging
- Add JSDoc comments
Step 4: Define JSON Schemas
Your inputSchema should validate all inputs:
inputSchema:
type: object
properties:
stringParam:
type: string
description: "A string parameter"
numberParam:
type: number
description: "A number parameter"
optionalParam:
type: string
enum: [option1, option2, option3]
default: option1
description: "An optional parameter"
required:
- stringParam
- numberParam
additionalProperties: falseSupported types: string, number, integer, boolean, array, object
Useful keywords: enum, default, minItems, maxItems, minimum, maximum
Step 5: Add Tests
Create tests to verify your functions work:
const { myFunction } = require('../src/my-function');
describe('myFunction', () => {
it('should handle typical case', async () => {
const result = await myFunction({ param1: 'test' });
expect(result).toBeDefined();
});
it('should handle edge cases', async () => {
// Test edge cases
});
});Run tests with:
npm testStep 6: Test Locally
Install the bridge and test your package:
# Install dependencies
npm install
# Test with the bridge
npx @backstack/mcp-yaml-bridge .Send test requests:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npx @backstack/mcp-yaml-bridge .Step 7: Publish to NPM
When ready to publish:
# Login to NPM
npm login
# Publish your package
npm publish --access public
# For scoped packages (@org/name), you need --access publicAfter publishing, you can use it in Backstack by registering the NPM package name.
Best Practices
Function Design
✅ DO:
- Use async functions consistently
- Destructure args with defaults
- Return plain objects
- Include timestamps
- Handle errors with clear messages
- Validate critical inputs beyond schema
❌ DON'T:
- Use sync functions (inconsistent with patterns)
- Mutate input arguments
- Return class instances or complex types
- Use console.log (throw errors instead)
- Rely solely on schema validation
Schema Design
✅ DO:
- Always set
additionalProperties: false - Use descriptive property names
- Provide helpful descriptions (10+ chars)
- Use enums for limited choices
- Set defaults for optional parameters
- Use appropriate constraints (min, max, minItems, etc.)
❌ DON'T:
- Allow additional properties (security risk)
- Use vague descriptions
- Over-constrain inputs unnecessarily
- Forget required fields array
Error Handling
✅ DO:
- Throw descriptive errors
- Validate edge cases
- Include context in error messages
- Document common errors in README
❌ DON'T:
- Silently fail or return error codes
- Use generic error messages
- Catch errors without rethrowing
- Assume inputs are always valid
Testing
✅ DO:
- Test success cases
- Test edge cases (empty, zero, negative, etc.)
- Test error scenarios
- Use descriptive test names
- Aim for high coverage
❌ DON'T:
- Only test happy paths
- Skip integration tests
- Use unclear test descriptions
Environment Variables
✅ DO:
- Declare required vars in YAML
environmentarray - Check if vars exist before use
- Provide sensible defaults when possible
- Document required variables
❌ DON'T:
- Forget to declare vars in YAML
- Assume vars are always set
- Store secrets in code
Architecture Patterns
Sync vs Async Functions
Always use async functions, even if they don't need await:
// Good
async function myFunction(args) {
const result = doSomething();
return { result };
}
// Avoid
function myFunction(args) {
return { result: doSomething() };
}Why? Consistency with the pattern makes code predictable and future-proof.
Destructuring Args
Always destructure with defaults:
// Good
async function myFunction(args) {
const { param1, param2 = 'default' } = args;
// ...
}
// Avoid
async function myFunction(args) {
const param1 = args.param1;
const param2 = args.param2 || 'default';
// ...
}Return Value Formats
Return plain objects with consistent structure:
// Good
return {
operation: 'action_name',
inputs: { param1, param2 },
result: computedValue,
metadata: { /* ... */ },
timestamp: new Date().toISOString()
};
// Avoid
return computedValue; // Too simple
return new ResultClass(value); // Don't use classesError Propagation
Let errors bubble up with clear messages:
// Good
if (invalidCondition) {
throw new Error('Clear message explaining what went wrong');
}
// Avoid
if (invalidCondition) {
return { error: 'Something failed' };
}Advanced Topics
Using External Dependencies
You can use any NPM package in your functions:
const axios = require('axios');
const _ = require('lodash');
async function myFunction(args) {
const response = await axios.get('https://api.example.com');
const processed = _.groupBy(response.data, 'category');
return { result: processed };
}Just add dependencies to your package.json:
npm install axios lodashTypeScript Support
You can write functions in TypeScript:
- Install TypeScript:
npm install --save-dev typescript ts-node - Write
.tsfiles instead of.js - Update
mcp-tools.yamlto point to.tsfiles - The bridge will automatically compile with
ts-node
Example:
interface Args {
param1: string;
param2?: number;
}
async function myFunction(args: Args): Promise<object> {
const { param1, param2 = 0 } = args;
return { result: param1, count: param2 };
}
export { myFunction };Multi-File Organization
Organize complex packages with multiple files:
src/
├── utils/
│ ├── validators.js
│ └── formatters.js
├── tools/
│ ├── tool1.js
│ ├── tool2.js
│ └── tool3.js
└── index.jsImport utilities in your tool files:
const { validateEmail } = require('../utils/validators');
async function myTool(args) {
if (!validateEmail(args.email)) {
throw new Error('Invalid email format');
}
// ...
}Environment Variable Management
Best practices for handling secrets:
async function myFunction(args) {
// Check if required env var exists
const apiKey = process.env.MY_API_KEY;
if (!apiKey) {
throw new Error('MY_API_KEY environment variable is required');
}
// Optional env var with fallback
const apiUrl = process.env.API_URL || 'https://api.default.com';
// Use the variables
const response = await fetch(apiUrl, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
return { result: response.data };
}Declare in YAML:
environment:
- MY_API_KEY # Required
# API_URL is optional, don't list itValidation Strategies
Layer validation for robustness:
async function myFunction(args) {
// 1. Schema validates basic types
// 2. Add business logic validation
const { email, age } = args;
if (!email.includes('@')) {
throw new Error('Email must contain @ symbol');
}
if (age < 0 || age > 150) {
throw new Error('Age must be between 0 and 150');
}
// 3. Proceed with validated inputs
return { valid: true };
}Troubleshooting
Common Issues
1. "Cannot find module" Error
Problem: Bridge can't locate your package or implementation file.
Solution:
- Verify
implementationpath in YAML is correct (e.g.,./src/my-file.js) - Ensure file exists at specified path
- Check file extension matches (
.js,.ts,.mjs,.cjs)
2. "Function not found" Error
Problem: Bridge can't find the exported function.
Solution:
- Verify function name in YAML matches export:
function: myFunction - Check you're using
module.exports = { myFunction } - Ensure function name is spelled correctly (case-sensitive)
3. Schema Validation Fails
Problem: Input arguments don't match schema.
Solution:
- Check required fields are provided
- Verify types match (number vs string)
- Ensure enum values are exact matches
- Remove any extra properties if
additionalProperties: false
4. Missing Environment Variable
Problem: Function expects env var that isn't set.
Solution:
- Set the variable:
export MY_VAR=value - Add to YAML
environmentarray if required - Check for typos in variable name
5. Invalid YAML Syntax
Problem: YAML file can't be parsed.
Solution:
- Check indentation (use spaces, not tabs)
- Verify colons have spaces after them:
name: value - Ensure strings with special chars are quoted
- Validate YAML with online validator
6. Tests Failing
Problem: Jest tests don't pass.
Solution:
- Run
npm installto install dependencies - Check test file paths are correct
- Verify function exports match imports
- Review error messages for specifics
7. Bridge Hangs or Doesn't Respond
Problem: No output from bridge command.
Solution:
- Ensure you're piping valid JSON-RPC to stdin
- Check for syntax errors in request
- Verify package name is correct
- Try running with explicit package path:
mcp-yaml-bridge ./
8. Unexpected Tool Behavior
Problem: Tool returns wrong results.
Solution:
- Add logging:
console.error('Debug:', value)(goes to stderr) - Write unit tests to isolate issue
- Check for async/await issues
- Verify input destructuring is correct
9. TypeScript Errors
Problem: .ts files don't work with bridge.
Solution:
- Install:
npm install --save-dev ts-node typescript - Verify TypeScript config exists (
tsconfig.json) - Check
ts-nodeis in devDependencies - Try compiling manually:
npx tsc
10. NPM Publish Fails
Problem: Can't publish package to NPM.
Solution:
- Login first:
npm login - For scoped packages:
npm publish --access public - Check package name isn't taken
- Verify version number is incremented
Debugging Tips
Enable verbose logging:
NODE_DEBUG=* mcp-yaml-bridge @backstack/exampleTest function directly:
const { myFunction } = require('./src/my-file');
myFunction({ test: 'args' }).then(console.log);Validate YAML:
npm install -g js-yaml
js-yaml mcp-tools.yamlCheck JSON-RPC format:
// Valid format:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "my_tool",
"arguments": { "param": "value" }
}
}API Reference
YAML Schema Reference
Complete specification for mcp-tools.yaml:
version: "1.0" # Required: Must be "1.0"
name: "string" # Required: Package name (should match package.json)
description: "string" # Optional: Package description
tools: # Required: Array of tool definitions (min 1)
- name: "string" # Required: Tool name (snake_case, lowercase, must match ^[a-z_][a-z0-9_]*$)
description: "string" # Required: Min 10 characters, used by AI agents
implementation: "string" # Required: Relative path to .js/.ts file (e.g., ./src/tool.js)
function: "string" # Required: Exported function name (camelCase)
inputSchema: # Required: JSON Schema object
type: object
properties: # Optional: Object defining parameters
paramName:
type: "string|number|integer|boolean|array|object"
description: "string"
enum: [] # Optional: Allowed values
default: value # Optional: Default value
# ... other JSON Schema keywords
required: [] # Optional: Array of required property names
additionalProperties: false # Recommended: Reject unknown properties
environment: # Optional: Array of required environment variable names
- "ENV_VAR_NAME"Supported JSON Schema Keywords
Type Keywords:
type: string, number, integer, boolean, array, object, nullenum: Array of allowed valuesconst: Single allowed value
Validation Keywords:
- Numbers:
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf - Strings:
minLength,maxLength,pattern(regex) - Arrays:
minItems,maxItems,uniqueItems - Objects:
required,properties,additionalProperties,minProperties,maxProperties
Annotation Keywords:
description: Human-readable descriptiontitle: Short titledefault: Default value (applied by validator)examples: Array of example values
Combining Schemas:
allOf: Must match all schemasanyOf: Must match at least one schemaoneOf: Must match exactly one schemanot: Must not match schema
MCP Bridge Behavior
Tool Discovery:
- Bridge resolves package using Node.js
require.resolve() - Reads
mcp-tools.yamlfrom package root - Validates YAML against JSON schema
- Checks all implementation files exist
Tool Execution:
- Validates arguments against
inputSchema(with type coercion and defaults) - Checks required environment variables exist
- Dynamically imports implementation module
- Executes named function with args object
- Returns result in MCP format
Error Handling:
- YAML validation errors → Detailed error with line number
- Missing files → "Implementation file not found" with path
- Missing function → "Function X not found in module Y"
- Schema validation errors → Detailed JSON Schema error
- Runtime errors → Stack trace in response
Contributing
We welcome contributions to improve this example package!
Reporting Issues
Found a bug or have a suggestion?
- Check existing issues: https://github.com/backstack-io/backstack-v2/issues
- Create a new issue with:
- Clear description
- Steps to reproduce (for bugs)
- Expected vs actual behavior
- Environment details (Node version, OS)
Pull Requests
Want to improve the code or documentation?
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-improvement - Make your changes
- Add tests if applicable
- Run tests:
npm test - Commit with clear message:
git commit -m "feat: add X" - Push and create a pull request
Code of Conduct
Be respectful and constructive. We're all here to learn and build great tools together.
License
MIT License - see LICENSE file for details.
Copyright (c) 2024 Backstack Team
Need Help?
- Documentation: https://docs.backstack.io
- Issues: https://github.com/backstack-io/backstack-v2/issues
- Discord: Join our community for questions and discussions
Made with ❤️ by the Backstack Team
