Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,44 @@ CircledIconView(image: Image("flag"), width: 40, strokeColor: Color.red)
<a href="https://www.watchto5k.com/">WatchTo5K</a>


## MCP Server (AI Tool Integration)

StepperView ships with a Swift-based [Model Context Protocol](https://modelcontextprotocol.io) server that lets any MCP-compatible AI tool (Claude Desktop, Cursor, VS Code) generate production-ready StepperView code directly in your workflow — no example app required.

### Build

```bash
cd StepperViewMCP
swift build -c release
```

### Tools

| Tool | Description |
|---|---|
| `generate_stepper_code` | Generate a complete SwiftUI `StepperView` struct from structured parameters |
| `list_colors` | List all valid named colors + hex color support |
| `list_sf_symbols` | List the curated SF Symbol allowlist |
| `get_example` | Return copy-paste Swift code for common patterns (`vertical`, `horizontal`, `pit_stops`, `hex_color`, `mixed_lifecycle`) |

### Claude Desktop Integration

Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
"mcpServers": {
"stepperview": {
"command": "/path/to/StepperView/StepperViewMCP/.build/release/StepperViewMCP"
}
}
}
```

Then ask Claude: *"Generate a 4-step onboarding stepper in teal with numbered circle indicators"*

---

## Author

Badarinath Venkatnarayansetty.Follow and contact me on <a href="https://twitter.com/badrivm">Twitter</a> or <a href="https://www.linkedin.com/in/badarinath-venkatnarayansetty-abb79146/">LinkedIn</a>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions StepperViewMCP/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "StepperViewMCP",
platforms: [
.macOS(.v14)
],
dependencies: [
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0")
],
targets: [
.executableTarget(
name: "StepperViewMCP",
dependencies: [
.product(name: "MCP", package: "swift-sdk"),
.product(name: "Yams", package: "Yams")
],
resources: [
.process("Resources")
],
swiftSettings: [
.swiftLanguageMode(.v6)
]
)
]
)
93 changes: 93 additions & 0 deletions StepperViewMCP/Sources/StepperViewMCP/Config/YAMLLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// YAMLLoader.swift
// StepperViewMCP
//
// Loads ai_config.yaml from the bundle at startup and exposes typed config
// values used by all four MCP tools.
//

import Foundation
import Yams

// MARK: - Public config types

struct ConfigDefaults: Sendable {
let defaultStepCount: Int
let defaultMode: String
let defaultSpacing: Int
let indicatorSize: Double
}

struct AppConfig: Sendable {
let validColors: [String]
let validSFSymbols: [String]
let validIndicatorTypes: [String]
let pitStopKeywords: [String]
let defaults: ConfigDefaults
}

// MARK: - Raw YAML Codable models (mirrors StepperAIConfig in the main library)

private struct RawConfig: Codable {
let defaults: RawDefaults
let validColors: [String]
let validIndicatorTypes: [String]
let validSFSymbols: [String]
let pitStopKeywords: [String]
}

private struct RawDefaults: Codable {
let defaultStepCount: Int
let defaultMode: String
let defaultSpacing: Int
let indicatorSize: Double
}

// MARK: - Loader

enum YAMLLoader {
static func load() -> AppConfig {
if let url = Bundle.module.url(forResource: "ai_config", withExtension: "yaml"),
let yamlString = try? String(contentsOf: url, encoding: .utf8),
let raw = try? YAMLDecoder().decode(RawConfig.self, from: yamlString) {
return AppConfig(
validColors: raw.validColors,
validSFSymbols: raw.validSFSymbols,
validIndicatorTypes: raw.validIndicatorTypes,
pitStopKeywords: raw.pitStopKeywords,
defaults: ConfigDefaults(
defaultStepCount: raw.defaults.defaultStepCount,
defaultMode: raw.defaults.defaultMode,
defaultSpacing: raw.defaults.defaultSpacing,
indicatorSize: raw.defaults.indicatorSize
)
)
}

// Fallback: hardcoded values that mirror ai_config.yaml
fputs("[StepperViewMCP] Warning: failed to load ai_config.yaml — using built-in defaults.\n", stderr)
return AppConfig(
validColors: ["teal", "red", "green", "blue", "orange", "lavender", "cyan", "black", "yellow", "polar"],
validSFSymbols: [
"checkmark.circle.fill", "xmark.circle.fill", "star.fill", "heart.fill",
"bell.fill", "flag.fill", "bookmark.fill", "tag.fill", "bolt.fill", "cart.fill",
"house.fill", "gear", "person.fill", "envelope.fill", "phone.fill",
"mappin.circle.fill", "location.fill", "clock.fill", "calendar", "alarm.fill",
"shippingbox.fill", "cube.fill", "gift.fill", "creditcard.fill", "banknote.fill",
"doc.text.fill", "folder.fill", "tray.fill", "archivebox.fill", "paperplane.fill",
"leaf.fill", "drop.fill", "flame.fill", "snowflake", "sun.max.fill",
"moon.fill", "cloud.fill", "wrench.fill", "hammer.fill", "paintbrush.fill",
"scissors", "lock.fill", "key.fill", "wifi", "antenna.radiowaves.left.and.right",
"airplane", "car.fill", "bus.fill", "bicycle", "figure.walk"
],
validIndicatorTypes: ["numberedCircle", "circle", "sfSymbol"],
pitStopKeywords: ["pit stop", "pit stops", "pitstop", "pitstops", "description", "details", "subtitles"],
defaults: ConfigDefaults(
defaultStepCount: 5,
defaultMode: "vertical",
defaultSpacing: 50,
indicatorSize: 40
)
)
}
}
124 changes: 124 additions & 0 deletions StepperViewMCP/Sources/StepperViewMCP/Resources/ai_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# AI Stepper Configuration
# Edit this file to customize the AI generation behavior and UI.

# UI Strings
ui:
navigationTitle: "StepperView AI"
headerText: "Describe your stepper"
placeholder: "e.g., Create a delivery tracking stepper with 5 stops"
generateButton: "Generate StepperView"
generatingButton: "Generating..."
previewTab: "Preview"
codeTab: "Code"
copyButton: "Copy"
errorPrefix: "Generation failed: "

# Default values for stepper generation
defaults:
defaultStepCount: 5
defaultMode: "vertical"
defaultSpacing: 50
indicatorWidthMin: 30
indicatorWidthMax: 40
indicatorSize: 40
pitStopHeightPerStep: 140
defaultHeightPerStep: 80
horizontalHeight: 160

# Named colors the AI model can use (hex colors like "#FF6B35" are also supported)
validColors:
- teal
- red
- green
- blue
- orange
- lavender
- cyan
- black
- yellow
- polar

# Valid indicator types
validIndicatorTypes:
- numberedCircle
- circle
- sfSymbol

# Curated list of valid SF Symbols
# Only these symbols are allowed — the model cannot invent names outside this list.
validSFSymbols:
- checkmark.circle.fill
- xmark.circle.fill
- star.fill
- heart.fill
- bell.fill
- flag.fill
- bookmark.fill
- tag.fill
- bolt.fill
- cart.fill
- house.fill
- gear
- person.fill
- envelope.fill
- phone.fill
- mappin.circle.fill
- location.fill
- clock.fill
- calendar
- alarm.fill
- shippingbox.fill
- cube.fill
- gift.fill
- creditcard.fill
- banknote.fill
- doc.text.fill
- folder.fill
- tray.fill
- archivebox.fill
- paperplane.fill
- leaf.fill
- drop.fill
- flame.fill
- snowflake
- sun.max.fill
- moon.fill
- cloud.fill
- wrench.fill
- hammer.fill
- paintbrush.fill
- scissors
- lock.fill
- key.fill
- wifi
- antenna.radiowaves.left.and.right
- airplane
- car.fill
- bus.fill
- bicycle
- figure.walk

# Keywords in user prompt that trigger pit stop generation
pitStopKeywords:
- "pit stop"
- "pit stops"
- "pitstop"
- "pitstops"
- "description"
- "details"
- "subtitles"

# System prompt template
# Uses {placeholders} that get replaced with values from this config.
systemPrompt: |
You are a StepperView configuration generator. Given a user's description, produce a structured stepper configuration. Follow these rules:
- Generate exactly {defaultStepCount} steps unless the user specifies a different number.
- Default to {defaultMode} mode with spacing {defaultSpacing}.
- For colors, you can use named colors ({validColors}) OR any hex color string (e.g. "#FF6B35", "#8B5CF6", "#EC4899"). Prefer hex colors when the user describes a specific color theme. Use the SAME color for ALL steps.
- Use indicatorType values: {validIndicatorTypes}. IMPORTANT: Use the SAME indicatorType for ALL steps. Do not mix different indicator types.
- When using sfSymbol, you MUST only use SF Symbol names from this list: {validSFSymbols}. Do NOT invent or guess symbol names outside this list.
- Set indicatorWidth between {indicatorWidthMin} and {indicatorWidthMax}.
- Mark earlier steps as completed and later steps as pending to show progress.
- IMPORTANT: Only include pitStopText when the user explicitly mentions "pit stop", "pit stops", "description", "details", or "subtitles" in their prompt. By default, do NOT include pitStopText. When pitStopText is omitted, set it to null for all steps. When pitStopText IS included, you MUST provide it for ALL steps with non-empty strings.
- Keep step titles concise (2-4 words).
- Set autoSpacing to true when pitStopText is provided, false otherwise.
81 changes: 81 additions & 0 deletions StepperViewMCP/Sources/StepperViewMCP/StepperMCPServer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// StepperMCPServer.swift
// StepperViewMCP
//
// Registers the four MCP tools and starts the stdio transport.
//

import MCP
import Foundation

// MARK: - Server

struct StepperMCPServer: Sendable {

private let config: AppConfig

init(config: AppConfig) {
self.config = config
}

func run() async throws {
let server = Server(
name: "StepperView MCP",
version: "1.0.0",
capabilities: .init(tools: .init(listChanged: false))
)

let allTools: [Tool] = [
GenerateCodeTool.definition,
ListColorsTool.definition,
ListSymbolsTool.definition,
GetExampleTool.definition
]

let config = self.config // capture value type for Sendable closures

await server.withMethodHandler(ListTools.self) { _ in
return .init(tools: allTools)
}

await server.withMethodHandler(CallTool.self) { params in
do {
return try Self.dispatch(params: params, config: config)
} catch {
return .init(
content: [.text("Error: \(error.localizedDescription)")],
isError: true
)
}
}

let transport = StdioTransport()
// start() launches a background message-handling Task and returns immediately.
// waitUntilCompleted() blocks until stdin closes (i.e. the MCP client disconnects).
try await server.start(transport: transport)
await server.waitUntilCompleted()
}

// MARK: - Dispatch

private static func dispatch(
params: CallTool.Parameters,
config: AppConfig
) throws -> CallTool.Result {
switch params.name {
case GenerateCodeTool.toolName:
return GenerateCodeTool.handle(params: params, config: config)
case ListColorsTool.toolName:
return ListColorsTool.handle(config: config)
case ListSymbolsTool.toolName:
return ListSymbolsTool.handle(config: config)
case GetExampleTool.toolName:
return GetExampleTool.handle(params: params)
default:
return .init(
content: [.text("Unknown tool: '\(params.name)'. Available: generate_stepper_code, list_colors, list_sf_symbols, get_example")],
isError: true
)
}
}
}
Loading
Loading