mcpc-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mcpc/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """mcpc — CLI for the MCP Contract specification."""
2
+
3
+ __version__ = "0.1.0"
mcpc/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m mcpc`."""
2
+
3
+ from mcpc.cli import main
4
+
5
+ main()
mcpc/cli.py ADDED
@@ -0,0 +1,172 @@
1
+ """mcpc CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from mcpc import __version__
9
+ from mcpc.init import TEMPLATES, init_bundle
10
+ from mcpc.pack import pack_bundle
11
+ from mcpc.test import test_bundle
12
+ from mcpc.unpack import unpack_bundle
13
+ from mcpc.validate import validate_bundle
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> None:
17
+ parser = argparse.ArgumentParser(
18
+ prog="mcpc",
19
+ description="CLI for the MCP Contract specification.",
20
+ )
21
+ parser.add_argument(
22
+ "--version", action="version", version=f"mcpc {__version__}"
23
+ )
24
+
25
+ subparsers = parser.add_subparsers(dest="command")
26
+
27
+ # -- validate --
28
+ validate_parser = subparsers.add_parser(
29
+ "validate",
30
+ help="Validate an MCP Contract bundle.",
31
+ description=(
32
+ "Validates the manifest against the schema, checks that all "
33
+ "provides/consumes contracts are satisfied, and verifies that "
34
+ "referenced files exist."
35
+ ),
36
+ )
37
+ validate_parser.add_argument(
38
+ "path",
39
+ nargs="?",
40
+ default=".",
41
+ help="Path to the bundle directory (default: current directory).",
42
+ )
43
+ validate_parser.add_argument(
44
+ "--quiet",
45
+ action="store_true",
46
+ help="Only print errors, suppress informational output.",
47
+ )
48
+
49
+ # -- init --
50
+ init_parser = subparsers.add_parser(
51
+ "init",
52
+ help="Scaffold a new MCP Contract bundle.",
53
+ description="Creates a new bundle directory with manifest and starter files.",
54
+ )
55
+ init_parser.add_argument(
56
+ "name",
57
+ help="Bundle name (lowercase, hyphens only).",
58
+ )
59
+ init_parser.add_argument(
60
+ "--path",
61
+ default=".",
62
+ help="Parent directory for the new bundle (default: current directory).",
63
+ )
64
+ init_parser.add_argument(
65
+ "--template",
66
+ choices=list(TEMPLATES.keys()),
67
+ default="full",
68
+ help="Bundle template (default: full).",
69
+ )
70
+
71
+ # -- pack --
72
+ pack_parser = subparsers.add_parser(
73
+ "pack",
74
+ help="Pack a bundle into a .mcpc archive.",
75
+ description=(
76
+ "Validates the bundle, then creates a .mcpc archive (zip) "
77
+ "containing all bundle files."
78
+ ),
79
+ )
80
+ pack_parser.add_argument(
81
+ "path",
82
+ nargs="?",
83
+ default=".",
84
+ help="Path to the bundle directory (default: current directory).",
85
+ )
86
+ pack_parser.add_argument(
87
+ "--output", "-o",
88
+ default=None,
89
+ help="Output file path (default: <name>-<version>.mcpc in parent directory).",
90
+ )
91
+ pack_parser.add_argument(
92
+ "--quiet",
93
+ action="store_true",
94
+ help="Only print errors, suppress informational output.",
95
+ )
96
+
97
+ # -- test --
98
+ test_parser = subparsers.add_parser(
99
+ "test",
100
+ help="Run structural tests on bundle layers.",
101
+ description=(
102
+ "Tests layer content quality: prompt frontmatter, schema "
103
+ "conventions, tool syntax, and app structure."
104
+ ),
105
+ )
106
+ test_parser.add_argument(
107
+ "path",
108
+ nargs="?",
109
+ default=".",
110
+ help="Path to the bundle directory (default: current directory).",
111
+ )
112
+ test_parser.add_argument(
113
+ "--layer",
114
+ choices=["prompts", "schemas", "tools", "apps"],
115
+ default=None,
116
+ help="Test only a specific layer.",
117
+ )
118
+ test_parser.add_argument(
119
+ "--quiet",
120
+ action="store_true",
121
+ help="Only print failures, suppress informational output.",
122
+ )
123
+
124
+ # -- unpack --
125
+ unpack_parser = subparsers.add_parser(
126
+ "unpack",
127
+ help="Extract a .mcpc archive into a bundle directory.",
128
+ description=(
129
+ "Extracts a .mcpc archive, verifies it contains a valid "
130
+ "manifest, and writes the bundle to a directory."
131
+ ),
132
+ )
133
+ unpack_parser.add_argument(
134
+ "archive",
135
+ help="Path to the .mcpc archive.",
136
+ )
137
+ unpack_parser.add_argument(
138
+ "--output", "-o",
139
+ default=None,
140
+ help="Output directory (default: archive stem in current directory).",
141
+ )
142
+ unpack_parser.add_argument(
143
+ "--quiet",
144
+ action="store_true",
145
+ help="Only print errors, suppress informational output.",
146
+ )
147
+
148
+ args = parser.parse_args(argv)
149
+
150
+ if args.command is None:
151
+ parser.print_help()
152
+ sys.exit(0)
153
+
154
+ if args.command == "validate":
155
+ ok = validate_bundle(args.path, quiet=args.quiet)
156
+ sys.exit(0 if ok else 1)
157
+
158
+ if args.command == "init":
159
+ ok = init_bundle(args.name, args.path, args.template)
160
+ sys.exit(0 if ok else 1)
161
+
162
+ if args.command == "pack":
163
+ ok = pack_bundle(args.path, output=args.output, quiet=args.quiet)
164
+ sys.exit(0 if ok else 1)
165
+
166
+ if args.command == "test":
167
+ ok = test_bundle(args.path, layer=args.layer, quiet=args.quiet)
168
+ sys.exit(0 if ok else 1)
169
+
170
+ if args.command == "unpack":
171
+ ok = unpack_bundle(args.archive, output=args.output, quiet=args.quiet)
172
+ sys.exit(0 if ok else 1)
mcpc/init.py ADDED
@@ -0,0 +1,300 @@
1
+ """Scaffold a new MCP Contract bundle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ _BOLD = "\033[1m"
12
+ _GREEN = "\033[32m"
13
+ _CYAN = "\033[36m"
14
+ _RESET = "\033[0m"
15
+
16
+
17
+ def _supports_color() -> bool:
18
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
19
+
20
+
21
+ def _fmt(code: str, text: str) -> str:
22
+ return f"{code}{text}{_RESET}" if _supports_color() else text
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Templates
27
+ # ---------------------------------------------------------------------------
28
+
29
+ TEMPLATES = {
30
+ "full": {
31
+ "description": "Complete bundle with all layers (prompts, tools, apps, skills)",
32
+ "layers": ["prompts", "tools", "apps", "skills", "schemas"],
33
+ },
34
+ "prompts-only": {
35
+ "description": "Minimal bundle with prompts layer only — no tools, apps, or skills",
36
+ "layers": ["prompts", "schemas"],
37
+ },
38
+ }
39
+
40
+
41
+ def _make_manifest(name: str, template: str) -> dict:
42
+ manifest: dict = {
43
+ "$schema": "https://mcpcontracts.com/schema/0.1.0.json",
44
+ "name": name,
45
+ "version": "0.1.0",
46
+ "description": "",
47
+ "author": {"name": ""},
48
+ "license": "Apache-2.0",
49
+ "layers": {},
50
+ }
51
+
52
+ layers = manifest["layers"]
53
+
54
+ if template in ("full", "prompts-only"):
55
+ layers["prompts"] = {
56
+ "entry": "prompts/main.md",
57
+ "provides": [
58
+ {
59
+ "name": f"{name}-output",
60
+ "schema": f"schemas/{name}-output.json",
61
+ "description": f"Output shape for {name} workflow.",
62
+ }
63
+ ],
64
+ }
65
+
66
+ if template == "full":
67
+ layers["tools"] = {
68
+ "entry": "tools/server.py",
69
+ "runtime": "python",
70
+ "provides": [
71
+ {
72
+ "name": f"{name}-data",
73
+ "schema": f"schemas/{name}-data.json",
74
+ "description": f"Data provided by {name} tool server.",
75
+ }
76
+ ],
77
+ }
78
+ layers["apps"] = {
79
+ "entry": "apps/main.html",
80
+ "consumes": [
81
+ {
82
+ "name": f"{name}-data",
83
+ "schema": f"schemas/{name}-data.json",
84
+ },
85
+ {
86
+ "name": f"{name}-output",
87
+ "schema": f"schemas/{name}-output.json",
88
+ },
89
+ ],
90
+ }
91
+ layers["skills"] = {
92
+ "targets": {
93
+ "claude": "skills/claude.md",
94
+ }
95
+ }
96
+ manifest["compiler_compatibility"] = ["claude"]
97
+ manifest["compose"] = {
98
+ "chain": ["prompts", "tools", "apps"],
99
+ "fallback": "prompts-only",
100
+ }
101
+ else:
102
+ manifest["compiler_compatibility"] = ["claude", "gpt", "gemini"]
103
+ manifest["compose"] = {
104
+ "chain": ["prompts"],
105
+ "fallback": "prompts-only",
106
+ }
107
+
108
+ return manifest
109
+
110
+
111
+ def _make_schema(name: str, title: str, description: str) -> dict:
112
+ return {
113
+ "$id": f"https://mcpcontracts.com/schemas/{name}/0.1.0",
114
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
115
+ "title": title,
116
+ "description": description,
117
+ "type": "object",
118
+ "required": [],
119
+ "properties": {},
120
+ }
121
+
122
+
123
+ def _make_prompt(bundle_name: str, provides_name: str) -> str:
124
+ return f"""---
125
+ name: {bundle_name}
126
+ version: 0.1.0
127
+ description: Primary methodology for {bundle_name}.
128
+ provides: {provides_name}
129
+ ---
130
+
131
+ # {bundle_name.replace('-', ' ').title()}
132
+
133
+ Describe your workflow's reasoning methodology here.
134
+
135
+ ## Methodology
136
+
137
+ 1. **Step one** — What to analyze first.
138
+ 2. **Step two** — How to structure the analysis.
139
+ 3. **Step three** — What output to produce.
140
+
141
+ ## Output Shape
142
+
143
+ The analysis produces structured output conforming to the
144
+ `{provides_name}` schema.
145
+ """
146
+
147
+
148
+ def _make_tool_stub(bundle_name: str) -> str:
149
+ return f'''"""
150
+ {bundle_name} MCP Tool Server (Stub)
151
+
152
+ Implement your MCP tools here. Each tool should:
153
+ - Declare typed input/output schemas in schemas/
154
+ - Not embed reasoning logic (that belongs in prompts/)
155
+ - Not assume a specific prompt or app layer
156
+ """
157
+ '''
158
+
159
+
160
+ def _make_app_stub(bundle_name: str) -> str:
161
+ return f"""<!--
162
+ {bundle_name} App (Stub)
163
+
164
+ Implement your interactive view here. The app should:
165
+ - Declare every schema it consumes in the manifest
166
+ - Not call external APIs directly (data flows through tools)
167
+ - Be renderable with mock data for independent testing
168
+ -->
169
+ <!DOCTYPE html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="UTF-8">
173
+ <title>{bundle_name.replace('-', ' ').title()}</title>
174
+ </head>
175
+ <body>
176
+ <h1>{bundle_name.replace('-', ' ').title()}</h1>
177
+ </body>
178
+ </html>
179
+ """
180
+
181
+
182
+ def _make_skill(bundle_name: str) -> str:
183
+ return f"""---
184
+ target: claude
185
+ version: 0.1.0
186
+ ---
187
+
188
+ # Claude Platform Skill
189
+
190
+ Platform-specific adaptation for running {bundle_name} on Claude.
191
+
192
+ ## Tool Registration
193
+
194
+ Register the tool server via MCP stdio transport in Claude Desktop config.
195
+
196
+ ## Context Window
197
+
198
+ Estimate your prompt chain token count here. Claude Opus supports 200k context.
199
+
200
+ ## Graceful Degradation
201
+
202
+ If the tool server is unavailable, the prompt layer should still produce
203
+ useful output from the LLM's training data.
204
+ """
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Public API
209
+ # ---------------------------------------------------------------------------
210
+
211
+ def init_bundle(name: str, path: str, template: str) -> bool:
212
+ """Scaffold a new MCP Contract bundle. Returns True on success."""
213
+ bundle_dir = Path(path).resolve() / name
214
+
215
+ if bundle_dir.exists():
216
+ print(f" error: {bundle_dir} already exists", file=sys.stderr)
217
+ return False
218
+
219
+ tmpl = TEMPLATES[template]
220
+ manifest = _make_manifest(name, template)
221
+
222
+ print(f"\n{_fmt(_BOLD, 'mcpc init')} {name} ({tmpl['description']})\n")
223
+
224
+ # Create directories
225
+ dirs = [bundle_dir / layer for layer in tmpl["layers"]]
226
+ for d in dirs:
227
+ d.mkdir(parents=True, exist_ok=True)
228
+
229
+ created: list[str] = []
230
+
231
+ # Write manifest
232
+ manifest_path = bundle_dir / "mcp-contract.json"
233
+ with open(manifest_path, "w") as f:
234
+ json.dump(manifest, f, indent=2)
235
+ f.write("\n")
236
+ created.append("mcp-contract.json")
237
+
238
+ # Write prompt
239
+ prompt_path = bundle_dir / "prompts" / "main.md"
240
+ with open(prompt_path, "w") as f:
241
+ f.write(_make_prompt(name, f"{name}-output"))
242
+ created.append("prompts/main.md")
243
+
244
+ # Write output schema
245
+ schema_path = bundle_dir / "schemas" / f"{name}-output.json"
246
+ with open(schema_path, "w") as f:
247
+ json.dump(
248
+ _make_schema(
249
+ f"{name}-output",
250
+ f"{name.replace('-', ' ').title()} Output",
251
+ f"Output shape for {name} workflow.",
252
+ ),
253
+ f,
254
+ indent=2,
255
+ )
256
+ f.write("\n")
257
+ created.append(f"schemas/{name}-output.json")
258
+
259
+ if template == "full":
260
+ # Data schema
261
+ data_schema_path = bundle_dir / "schemas" / f"{name}-data.json"
262
+ with open(data_schema_path, "w") as f:
263
+ json.dump(
264
+ _make_schema(
265
+ f"{name}-data",
266
+ f"{name.replace('-', ' ').title()} Data",
267
+ f"Data provided by {name} tool server.",
268
+ ),
269
+ f,
270
+ indent=2,
271
+ )
272
+ f.write("\n")
273
+ created.append(f"schemas/{name}-data.json")
274
+
275
+ # Tool stub
276
+ tool_path = bundle_dir / "tools" / "server.py"
277
+ with open(tool_path, "w") as f:
278
+ f.write(_make_tool_stub(name))
279
+ created.append("tools/server.py")
280
+
281
+ # App stub
282
+ app_path = bundle_dir / "apps" / "main.html"
283
+ with open(app_path, "w") as f:
284
+ f.write(_make_app_stub(name))
285
+ created.append("apps/main.html")
286
+
287
+ # Skill
288
+ skill_path = bundle_dir / "skills" / "claude.md"
289
+ with open(skill_path, "w") as f:
290
+ f.write(_make_skill(name))
291
+ created.append("skills/claude.md")
292
+
293
+ # Report
294
+ for f in created:
295
+ print(f" {_fmt(_GREEN, '+')} {f}")
296
+
297
+ print(f"\n{_fmt(_CYAN, ' info:')} created {len(created)} files in {bundle_dir}")
298
+ print(f"{_fmt(_CYAN, ' info:')} run {_fmt(_BOLD, f'mcpc validate {name}')} to check\n")
299
+
300
+ return True
mcpc/pack.py ADDED
@@ -0,0 +1,124 @@
1
+ """Pack an MCP Contract bundle into a .mcpc archive.
2
+
3
+ Validates the bundle first, then creates a zip archive with the .mcpc
4
+ extension containing all bundle files. The archive preserves the
5
+ directory structure relative to the bundle root.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+ from mcpc.validate import validate_bundle
17
+
18
+
19
+ _BOLD = "\033[1m"
20
+ _RED = "\033[31m"
21
+ _GREEN = "\033[32m"
22
+ _CYAN = "\033[36m"
23
+ _RESET = "\033[0m"
24
+
25
+
26
+ def _supports_color() -> bool:
27
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
28
+
29
+
30
+ def _fmt(code: str, text: str) -> str:
31
+ return f"{code}{text}{_RESET}" if _supports_color() else text
32
+
33
+
34
+ def _error(msg: str) -> str:
35
+ return _fmt(_RED, f" error: {msg}")
36
+
37
+
38
+ def _info(msg: str) -> str:
39
+ return _fmt(_CYAN, f" info: {msg}")
40
+
41
+
42
+ def _ok(msg: str) -> str:
43
+ return _fmt(_GREEN, f" ok: {msg}")
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # File collection
48
+ # ---------------------------------------------------------------------------
49
+
50
+ # Files/directories to always exclude from the archive
51
+ EXCLUDE_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv", ".mypy_cache"}
52
+ EXCLUDE_FILES = {".DS_Store", "Thumbs.db"}
53
+
54
+
55
+ def _collect_files(bundle_dir: Path) -> list[Path]:
56
+ """Collect all files in the bundle directory, excluding common junk."""
57
+ files: list[Path] = []
58
+ for root, dirs, filenames in os.walk(bundle_dir):
59
+ # Prune excluded directories in-place
60
+ dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")]
61
+ for name in filenames:
62
+ if name in EXCLUDE_FILES:
63
+ continue
64
+ files.append(Path(root) / name)
65
+ return sorted(files)
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Public API
70
+ # ---------------------------------------------------------------------------
71
+
72
+ def pack_bundle(path: str, output: str | None = None, quiet: bool = False) -> bool:
73
+ """Pack an MCP Contract bundle into a .mcpc archive. Returns True on success."""
74
+ bundle_dir = Path(path).resolve()
75
+
76
+ if not quiet:
77
+ print(f"\n{_fmt(_BOLD, 'mcpc pack')} {bundle_dir}\n")
78
+
79
+ # Step 1: Validate
80
+ if not quiet:
81
+ print(_info("validating bundle..."))
82
+
83
+ valid = validate_bundle(str(bundle_dir), quiet=True)
84
+ if not valid:
85
+ print(_error("bundle validation failed — run 'mcpc validate' for details"))
86
+ return False
87
+
88
+ if not quiet:
89
+ print(_ok("validation passed"))
90
+
91
+ # Step 2: Determine output path
92
+ manifest_path = bundle_dir / "mcp-contract.json"
93
+ with open(manifest_path) as f:
94
+ manifest = json.load(f)
95
+
96
+ name = manifest.get("name", "bundle")
97
+ version = manifest.get("version", "0.0.0")
98
+
99
+ if output is not None:
100
+ out_path = Path(output).resolve()
101
+ else:
102
+ out_path = bundle_dir.parent / f"{name}-{version}.mcpc"
103
+
104
+ # Step 3: Collect files
105
+ files = _collect_files(bundle_dir)
106
+
107
+ if not quiet:
108
+ print(_info(f"packing {len(files)} files..."))
109
+
110
+ # Step 4: Create archive
111
+ with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
112
+ for file_path in files:
113
+ arcname = file_path.relative_to(bundle_dir)
114
+ zf.write(file_path, arcname)
115
+
116
+ size_kb = out_path.stat().st_size / 1024
117
+
118
+ if not quiet:
119
+ print()
120
+ print(_ok(f"created {out_path.name} ({size_kb:.1f} KB)"))
121
+ print(_info(f"path: {out_path}"))
122
+ print()
123
+
124
+ return True