Plugin Development Guide
Build capabilities for new file types
Overview
Plugins are standalone executables that add capabilities to MachineFabric. A plugin can handle one file type (like PDFs) or many. It declares what it can do via a manifest, and MachineFabric calls it when those capabilities are needed.
What a plugin does
- Responds to
manifestcommand with its capabilities - Implements one or more capability commands (extract-metadata, grind, etc.)
- Reads input from command-line arguments or stdin
- Writes JSON output to stdout
Languages
Plugins can be written in any language that produces a native executable. We provide SDKs for:
- Rust -
machfab-plugin-sdkcrate - Go -
machfab-plugin-sdk-gomodule - Swift/Objective-C -
MACINAPluginSDKframework
The SDK is optional—you can write a plugin in Python, C++, or anything else as long as it follows the protocol.
Architecture
Plugins run in a sandboxed XPC service, separate from the main app. MachineFabric never loads plugin code directly.
Execution flow
- MachineFabric's XPC service discovers plugins at startup
- For each executable, it runs
plugin manifest - Parses the JSON manifest to learn what capabilities exist
- Registers capabilities with the router
- When a capability is needed, spawns the plugin with arguments
- Plugin writes output to stdout, errors to stderr
- XPC service captures output and returns it to the app
Plugin locations
The XPC service looks for plugins in two places:
MachineFabric.app/Contents/Resources/pluginsrv-signed/plugins/- Bundled/Library/Application Support/MachineFabric/Plugins/- User-installed
Security requirements
Plugins distributed via MachineFabric must be:
- Code-signed with a Developer ID certificate
- Notarized by Apple
- Packaged as a
.pkginstaller
For development, you can test unsigned plugins by placing them in the plugins directory and allowing them in System Settings.
Plugin Manifest
When called with manifest as the first argument, your plugin
must print a JSON object describing itself.
Manifest structure
{
"name": "myplugin",
"version": "1.0.0",
"description": "Processes XYZ files",
"authors": ["Your Name"],
"caps": [
{
"urn": "cap:op=extract;format=xyz;target=metadata",
"title": "Extract XYZ Metadata",
"description": "Extract metadata from XYZ files",
"command": "extract-metadata",
"arguments": {
"required": [...],
"optional": [...]
},
"output": {...},
"accepts_stdin": true
}
]
}
Fields
name |
Plugin identifier (lowercase, no spaces) |
version |
Semantic version (e.g., "1.0.0") |
description |
What the plugin does |
authors |
List of author names (optional) |
caps |
Array of capability definitions |
Example: Rust implementation
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Manifest,
ExtractMetadata { file: String },
}
fn main() {
let args = Args::parse();
match args.command {
Commands::Manifest => {
let manifest = get_plugin_manifest();
println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
}
Commands::ExtractMetadata { file } => {
// ... implementation
}
}
}
Declaring Capabilities
Each capability in your manifest describes one operation. Capabilities use CAPDAG URNs for identification.
Standard capabilities
MachineFabric recognizes these standard operations:
extract-metadata |
Extract file metadata (title, author, page count, etc.) |
extract-outline |
Extract table of contents / document structure |
grind |
Extract text content by page or section |
generate-thumbnail |
Generate preview image |
Capability URN format
URNs are built from tags that describe the operation:
cap:op=extract;format=pdf;target=metadata cap:op=extract;format=epub;target=pages cap:op=generate;format=pdf;target=thumbnail
Capability definition
{
"urn": "cap:op=extract;format=xyz;target=metadata",
"title": "Extract XYZ Metadata",
"description": "Extract metadata from XYZ files",
"command": "extract-metadata",
"metadata": {
"file_types": "xyz,abc"
},
"arguments": {
"required": [
{
"name": "file_path",
"media_spec": "media:string",
"arg_description": "Path to input file",
"cli_flag": "file_path",
"position": 0
}
],
"optional": [
{
"name": "output",
"media_spec": "media:string",
"arg_description": "Output file path",
"cli_flag": "--output"
}
]
},
"output": {
"media_spec": "media:object",
"output_description": "Extracted metadata as JSON"
},
"accepts_stdin": false
}
Argument types (media_spec)
media:string- Stringmedia:integer- Integermedia:number- Number (float)media:boolean- Booleanmedia:object- JSON objectmedia:binary- Binary data
Implementing Commands
Each capability maps to a command that your plugin handles.
The command field in the capability definition tells
MachineFabric how to invoke your plugin.
Command invocation
MachineFabric calls your plugin like this:
# Extract metadata ./myplugin extract-metadata /path/to/file.xyz # With optional output file ./myplugin extract-metadata /path/to/file.xyz --output metadata.json # Generate thumbnail ./myplugin generate-thumbnail /path/to/file.xyz --width 256 --height 256
Example: extract-metadata
fn extract_metadata(file_path: &str) -> Result<FileMetadata> {
// 1. Validate file exists and is readable
let path = Path::new(file_path);
if !path.exists() {
anyhow::bail!("File not found: {}", file_path);
}
// 2. Read and parse the file
let content = fs::read(file_path)?;
let parsed = parse_xyz_format(&content)?;
// 3. Build metadata structure
let mut metadata = FileMetadata::new(
file_path.to_string(),
"xyz".to_string(),
content.len() as u64,
);
metadata.title = parsed.title;
metadata.authors = parsed.authors;
metadata.page_count = Some(parsed.pages.len());
Ok(metadata)
}
fn main() {
match args.command {
Commands::ExtractMetadata { file, output } => {
match extract_metadata(&file) {
Ok(metadata) => {
let json = serde_json::to_string_pretty(&metadata).unwrap();
if let Some(out_path) = output {
fs::write(out_path, &json).unwrap();
} else {
println!("{}", json);
}
}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
}
}
}
Example: disbind
fn disbind(file_path: &str, index_range: Option<&str>) -> Result<Vec<DisboundPage>> {
let parsed = parse_xyz_file(file_path)?;
let pages_to_extract = parse_index_range(index_range, parsed.pages.len())?;
let mut pages = Vec::new();
for page_num in pages_to_extract {
let page_content = &parsed.pages[page_num];
let page = DisboundPage::new_with_text(
page_num + 1, // 1-indexed
page_content.text.clone(),
);
pages.push(page);
}
Ok(pages)
}
Input/Output
Input
Plugins receive input through:
- Command-line arguments - File paths, flags, options
- Stdin - If
accepts_stdin: true, can receive file content
Output
Plugins write output to:
- Stdout - JSON data or binary content
- Stderr - Progress messages, warnings, errors
- File - If
--outputflag is provided
Exit codes
0- Success1- General error2- Invalid arguments
Output formats
FileMetadata output
{
"file_path": "/path/to/file.xyz",
"file_size_bytes": 1048576,
"document_type": "xyz",
"title": "Document Title",
"authors": ["Author Name"],
"page_count": 42,
"creation_date": "2024-01-10T15:30:00Z",
"keywords": ["tag1", "tag2"],
"extended_metadata": {
"custom_field": "value"
}
}
DocumentOutline output
{
"source_file": "/path/to/file.xyz",
"document_type": "xyz",
"total_pages": 42,
"has_outline": true,
"entries": [
{
"title": "Chapter 1",
"level": 0,
"page": 1,
"children": [
{
"title": "Section 1.1",
"level": 1,
"page": 5,
"children": []
}
]
}
],
"extraction_info": {
"extractor_name": "myplugin",
"extractor_version": "1.0.0"
}
}
Disbind output (DisboundPage array)
[
{
"order_index": 1,
"text_content": "Content of page 1...",
"word_count": 250,
"character_count": 1500
},
{
"order_index": 2,
"text_content": "Content of page 2...",
"word_count": 300,
"character_count": 1800
}
]
Using the SDK
The SDK provides standard data structures and helpers. Using it ensures your output matches what MachineFabric expects.
Rust SDK
# Cargo.toml
[dependencies]
machfab-plugin-sdk = { git = "https://github.com/machfab/machfab-plugin-sdk" }
capdag = { git = "https://github.com/machfab/capdag" }
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
use machfab_plugin_sdk::{
FileMetadata, DocumentOutline,
OutlineEntry, DisboundPage,
};
// Create metadata
let mut metadata = FileMetadata::new(path, "xyz", file_size);
metadata.title = Some("Title".into());
metadata.add_author("Author");
// Create outline
let mut outline = DocumentOutline::new(path, "xyz", page_count);
let entry = OutlineEntry::new("Chapter 1", 0).with_page(1);
outline.add_entry(entry);
// Create pages (simple Vec)
let mut pages: Vec<DisboundPage> = Vec::new();
let page = DisboundPage::new_with_text(1, "Content...");
pages.push(page);
Go SDK
import sdk "github.com/machfab/machfab-plugin-sdk-go"
// Create metadata
metadata := sdk.NewFileMetadata(path, "xyz", fileSize)
metadata.Title = "Title"
metadata.AddAuthor("Author")
// Create outline
outline := sdk.NewDocumentOutline(path, "xyz", pageCount)
entry := sdk.NewOutlineEntry("Chapter 1", 0).WithPage(1)
outline.AddEntry(entry)
// Create pages (simple slice)
var pages []sdk.DisboundPage
page := sdk.NewDisboundPageWithText(1, "Content...")
pages = append(pages, *page)
SDK data structures
See SDK Reference for complete documentation.
Testing
Manual testing
# Test manifest ./myplugin manifest | jq . # Test extract-metadata ./myplugin extract-metadata test-file.xyz | jq . # Test with output file ./myplugin extract-metadata test-file.xyz -o output.json cat output.json | jq .
Validate manifest
The SDK includes a validator tool:
plugin-validator ./myplugin
Test with MachineFabric
- Build your plugin
- Copy to
/Library/Application Support/MachineFabric/Plugins/ - Restart MachineFabric
- Add a file of your supported type
- Check that grinding works
Packaging
Plugins are distributed as .pkg installers.
This handles macOS security requirements (Gatekeeper, quarantine).
Build requirements
- Universal binary (arm64 + x86_64) or Apple Silicon only
- Signed with Developer ID certificate
- Notarized by Apple
Creating a .pkg
# Build universal binary (Rust example) cargo build --release --target aarch64-apple-darwin cargo build --release --target x86_64-apple-darwin lipo -create -output myplugin \ target/aarch64-apple-darwin/release/myplugin \ target/x86_64-apple-darwin/release/myplugin # Sign the binary codesign --sign "Developer ID Application: Your Name (TEAMID)" \ --options runtime \ --timestamp \ myplugin # Create installer package pkgbuild --root ./pkg-root \ --identifier com.yourcompany.myplugin \ --version 1.0.0 \ --install-location "/Library/Application Support/MachineFabric/Plugins" \ myplugin.pkg # Sign the package productsign --sign "Developer ID Installer: Your Name (TEAMID)" \ myplugin.pkg \ myplugin-signed.pkg # Notarize xcrun notarytool submit myplugin-signed.pkg \ --apple-id your@email.com \ --team-id TEAMID \ --password "@keychain:AC_PASSWORD" \ --wait # Staple xcrun stapler staple myplugin-signed.pkg
Publishing
To make your plugin available through MachineFabric's plugin browser:
Requirements
- Plugin source code in a public GitHub repository
- Signed and notarized
.pkginstaller - README with description and usage
Submit for inclusion
Email plugins@machinefabric.com with:
- GitHub repository URL
- Link to downloadable
.pkgfile - Brief description of what file types it handles
We review submissions and add approved plugins to the registry. Future versions will automate this process.
Plugin registry format
The registry is a JSON file listing available plugins:
{
"plugins": [
{
"name": "myplugin",
"version": "1.0.0",
"description": "Processes XYZ files",
"download_url": "https://github.com/.../releases/download/v1.0.0/myplugin.pkg",
"sha256": "abc123...",
"file_types": ["xyz", "abc"],
"repository": "https://github.com/yourname/myplugin"
}
]
}