Skip to content

Commit 100b54c

Browse files
authored
Add StepperViewMCP — MCP server for StepperView code generation (#106)
1 parent d902bfc commit 100b54c

12 files changed

Lines changed: 1108 additions & 0 deletions

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,44 @@ CircledIconView(image: Image("flag"), width: 40, strokeColor: Color.red)
388388
<a href="https://www.watchto5k.com/">WatchTo5K</a>
389389

390390

391+
## MCP Server (AI Tool Integration)
392+
393+
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.
394+
395+
### Build
396+
397+
```bash
398+
cd StepperViewMCP
399+
swift build -c release
400+
```
401+
402+
### Tools
403+
404+
| Tool | Description |
405+
|---|---|
406+
| `generate_stepper_code` | Generate a complete SwiftUI `StepperView` struct from structured parameters |
407+
| `list_colors` | List all valid named colors + hex color support |
408+
| `list_sf_symbols` | List the curated SF Symbol allowlist |
409+
| `get_example` | Return copy-paste Swift code for common patterns (`vertical`, `horizontal`, `pit_stops`, `hex_color`, `mixed_lifecycle`) |
410+
411+
### Claude Desktop Integration
412+
413+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
414+
415+
```json
416+
{
417+
"mcpServers": {
418+
"stepperview": {
419+
"command": "/path/to/StepperView/StepperViewMCP/.build/release/StepperViewMCP"
420+
}
421+
}
422+
}
423+
```
424+
425+
Then ask Claude: *"Generate a 4-step onboarding stepper in teal with numbered circle indicators"*
426+
427+
---
428+
391429
## Author
392430

393431
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>

StepperViewMCP/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

StepperViewMCP/Package.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version: 6.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "StepperViewMCP",
6+
platforms: [
7+
.macOS(.v14)
8+
],
9+
dependencies: [
10+
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"),
11+
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0")
12+
],
13+
targets: [
14+
.executableTarget(
15+
name: "StepperViewMCP",
16+
dependencies: [
17+
.product(name: "MCP", package: "swift-sdk"),
18+
.product(name: "Yams", package: "Yams")
19+
],
20+
resources: [
21+
.process("Resources")
22+
],
23+
swiftSettings: [
24+
.swiftLanguageMode(.v6)
25+
]
26+
)
27+
]
28+
)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//
2+
// YAMLLoader.swift
3+
// StepperViewMCP
4+
//
5+
// Loads ai_config.yaml from the bundle at startup and exposes typed config
6+
// values used by all four MCP tools.
7+
//
8+
9+
import Foundation
10+
import Yams
11+
12+
// MARK: - Public config types
13+
14+
struct ConfigDefaults: Sendable {
15+
let defaultStepCount: Int
16+
let defaultMode: String
17+
let defaultSpacing: Int
18+
let indicatorSize: Double
19+
}
20+
21+
struct AppConfig: Sendable {
22+
let validColors: [String]
23+
let validSFSymbols: [String]
24+
let validIndicatorTypes: [String]
25+
let pitStopKeywords: [String]
26+
let defaults: ConfigDefaults
27+
}
28+
29+
// MARK: - Raw YAML Codable models (mirrors StepperAIConfig in the main library)
30+
31+
private struct RawConfig: Codable {
32+
let defaults: RawDefaults
33+
let validColors: [String]
34+
let validIndicatorTypes: [String]
35+
let validSFSymbols: [String]
36+
let pitStopKeywords: [String]
37+
}
38+
39+
private struct RawDefaults: Codable {
40+
let defaultStepCount: Int
41+
let defaultMode: String
42+
let defaultSpacing: Int
43+
let indicatorSize: Double
44+
}
45+
46+
// MARK: - Loader
47+
48+
enum YAMLLoader {
49+
static func load() -> AppConfig {
50+
if let url = Bundle.module.url(forResource: "ai_config", withExtension: "yaml"),
51+
let yamlString = try? String(contentsOf: url, encoding: .utf8),
52+
let raw = try? YAMLDecoder().decode(RawConfig.self, from: yamlString) {
53+
return AppConfig(
54+
validColors: raw.validColors,
55+
validSFSymbols: raw.validSFSymbols,
56+
validIndicatorTypes: raw.validIndicatorTypes,
57+
pitStopKeywords: raw.pitStopKeywords,
58+
defaults: ConfigDefaults(
59+
defaultStepCount: raw.defaults.defaultStepCount,
60+
defaultMode: raw.defaults.defaultMode,
61+
defaultSpacing: raw.defaults.defaultSpacing,
62+
indicatorSize: raw.defaults.indicatorSize
63+
)
64+
)
65+
}
66+
67+
// Fallback: hardcoded values that mirror ai_config.yaml
68+
fputs("[StepperViewMCP] Warning: failed to load ai_config.yaml — using built-in defaults.\n", stderr)
69+
return AppConfig(
70+
validColors: ["teal", "red", "green", "blue", "orange", "lavender", "cyan", "black", "yellow", "polar"],
71+
validSFSymbols: [
72+
"checkmark.circle.fill", "xmark.circle.fill", "star.fill", "heart.fill",
73+
"bell.fill", "flag.fill", "bookmark.fill", "tag.fill", "bolt.fill", "cart.fill",
74+
"house.fill", "gear", "person.fill", "envelope.fill", "phone.fill",
75+
"mappin.circle.fill", "location.fill", "clock.fill", "calendar", "alarm.fill",
76+
"shippingbox.fill", "cube.fill", "gift.fill", "creditcard.fill", "banknote.fill",
77+
"doc.text.fill", "folder.fill", "tray.fill", "archivebox.fill", "paperplane.fill",
78+
"leaf.fill", "drop.fill", "flame.fill", "snowflake", "sun.max.fill",
79+
"moon.fill", "cloud.fill", "wrench.fill", "hammer.fill", "paintbrush.fill",
80+
"scissors", "lock.fill", "key.fill", "wifi", "antenna.radiowaves.left.and.right",
81+
"airplane", "car.fill", "bus.fill", "bicycle", "figure.walk"
82+
],
83+
validIndicatorTypes: ["numberedCircle", "circle", "sfSymbol"],
84+
pitStopKeywords: ["pit stop", "pit stops", "pitstop", "pitstops", "description", "details", "subtitles"],
85+
defaults: ConfigDefaults(
86+
defaultStepCount: 5,
87+
defaultMode: "vertical",
88+
defaultSpacing: 50,
89+
indicatorSize: 40
90+
)
91+
)
92+
}
93+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# AI Stepper Configuration
2+
# Edit this file to customize the AI generation behavior and UI.
3+
4+
# UI Strings
5+
ui:
6+
navigationTitle: "StepperView AI"
7+
headerText: "Describe your stepper"
8+
placeholder: "e.g., Create a delivery tracking stepper with 5 stops"
9+
generateButton: "Generate StepperView"
10+
generatingButton: "Generating..."
11+
previewTab: "Preview"
12+
codeTab: "Code"
13+
copyButton: "Copy"
14+
errorPrefix: "Generation failed: "
15+
16+
# Default values for stepper generation
17+
defaults:
18+
defaultStepCount: 5
19+
defaultMode: "vertical"
20+
defaultSpacing: 50
21+
indicatorWidthMin: 30
22+
indicatorWidthMax: 40
23+
indicatorSize: 40
24+
pitStopHeightPerStep: 140
25+
defaultHeightPerStep: 80
26+
horizontalHeight: 160
27+
28+
# Named colors the AI model can use (hex colors like "#FF6B35" are also supported)
29+
validColors:
30+
- teal
31+
- red
32+
- green
33+
- blue
34+
- orange
35+
- lavender
36+
- cyan
37+
- black
38+
- yellow
39+
- polar
40+
41+
# Valid indicator types
42+
validIndicatorTypes:
43+
- numberedCircle
44+
- circle
45+
- sfSymbol
46+
47+
# Curated list of valid SF Symbols
48+
# Only these symbols are allowed — the model cannot invent names outside this list.
49+
validSFSymbols:
50+
- checkmark.circle.fill
51+
- xmark.circle.fill
52+
- star.fill
53+
- heart.fill
54+
- bell.fill
55+
- flag.fill
56+
- bookmark.fill
57+
- tag.fill
58+
- bolt.fill
59+
- cart.fill
60+
- house.fill
61+
- gear
62+
- person.fill
63+
- envelope.fill
64+
- phone.fill
65+
- mappin.circle.fill
66+
- location.fill
67+
- clock.fill
68+
- calendar
69+
- alarm.fill
70+
- shippingbox.fill
71+
- cube.fill
72+
- gift.fill
73+
- creditcard.fill
74+
- banknote.fill
75+
- doc.text.fill
76+
- folder.fill
77+
- tray.fill
78+
- archivebox.fill
79+
- paperplane.fill
80+
- leaf.fill
81+
- drop.fill
82+
- flame.fill
83+
- snowflake
84+
- sun.max.fill
85+
- moon.fill
86+
- cloud.fill
87+
- wrench.fill
88+
- hammer.fill
89+
- paintbrush.fill
90+
- scissors
91+
- lock.fill
92+
- key.fill
93+
- wifi
94+
- antenna.radiowaves.left.and.right
95+
- airplane
96+
- car.fill
97+
- bus.fill
98+
- bicycle
99+
- figure.walk
100+
101+
# Keywords in user prompt that trigger pit stop generation
102+
pitStopKeywords:
103+
- "pit stop"
104+
- "pit stops"
105+
- "pitstop"
106+
- "pitstops"
107+
- "description"
108+
- "details"
109+
- "subtitles"
110+
111+
# System prompt template
112+
# Uses {placeholders} that get replaced with values from this config.
113+
systemPrompt: |
114+
You are a StepperView configuration generator. Given a user's description, produce a structured stepper configuration. Follow these rules:
115+
- Generate exactly {defaultStepCount} steps unless the user specifies a different number.
116+
- Default to {defaultMode} mode with spacing {defaultSpacing}.
117+
- 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.
118+
- Use indicatorType values: {validIndicatorTypes}. IMPORTANT: Use the SAME indicatorType for ALL steps. Do not mix different indicator types.
119+
- When using sfSymbol, you MUST only use SF Symbol names from this list: {validSFSymbols}. Do NOT invent or guess symbol names outside this list.
120+
- Set indicatorWidth between {indicatorWidthMin} and {indicatorWidthMax}.
121+
- Mark earlier steps as completed and later steps as pending to show progress.
122+
- 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.
123+
- Keep step titles concise (2-4 words).
124+
- Set autoSpacing to true when pitStopText is provided, false otherwise.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// StepperMCPServer.swift
3+
// StepperViewMCP
4+
//
5+
// Registers the four MCP tools and starts the stdio transport.
6+
//
7+
8+
import MCP
9+
import Foundation
10+
11+
// MARK: - Server
12+
13+
struct StepperMCPServer: Sendable {
14+
15+
private let config: AppConfig
16+
17+
init(config: AppConfig) {
18+
self.config = config
19+
}
20+
21+
func run() async throws {
22+
let server = Server(
23+
name: "StepperView MCP",
24+
version: "1.0.0",
25+
capabilities: .init(tools: .init(listChanged: false))
26+
)
27+
28+
let allTools: [Tool] = [
29+
GenerateCodeTool.definition,
30+
ListColorsTool.definition,
31+
ListSymbolsTool.definition,
32+
GetExampleTool.definition
33+
]
34+
35+
let config = self.config // capture value type for Sendable closures
36+
37+
await server.withMethodHandler(ListTools.self) { _ in
38+
return .init(tools: allTools)
39+
}
40+
41+
await server.withMethodHandler(CallTool.self) { params in
42+
do {
43+
return try Self.dispatch(params: params, config: config)
44+
} catch {
45+
return .init(
46+
content: [.text("Error: \(error.localizedDescription)")],
47+
isError: true
48+
)
49+
}
50+
}
51+
52+
let transport = StdioTransport()
53+
// start() launches a background message-handling Task and returns immediately.
54+
// waitUntilCompleted() blocks until stdin closes (i.e. the MCP client disconnects).
55+
try await server.start(transport: transport)
56+
await server.waitUntilCompleted()
57+
}
58+
59+
// MARK: - Dispatch
60+
61+
private static func dispatch(
62+
params: CallTool.Parameters,
63+
config: AppConfig
64+
) throws -> CallTool.Result {
65+
switch params.name {
66+
case GenerateCodeTool.toolName:
67+
return GenerateCodeTool.handle(params: params, config: config)
68+
case ListColorsTool.toolName:
69+
return ListColorsTool.handle(config: config)
70+
case ListSymbolsTool.toolName:
71+
return ListSymbolsTool.handle(config: config)
72+
case GetExampleTool.toolName:
73+
return GetExampleTool.handle(params: params)
74+
default:
75+
return .init(
76+
content: [.text("Unknown tool: '\(params.name)'. Available: generate_stepper_code, list_colors, list_sf_symbols, get_example")],
77+
isError: true
78+
)
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)