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 +3 -0
- mcpc/__main__.py +5 -0
- mcpc/cli.py +172 -0
- mcpc/init.py +300 -0
- mcpc/pack.py +124 -0
- mcpc/test.py +311 -0
- mcpc/unpack.py +103 -0
- mcpc/validate.py +537 -0
- mcpc_cli-0.1.0.dist-info/METADATA +83 -0
- mcpc_cli-0.1.0.dist-info/RECORD +13 -0
- mcpc_cli-0.1.0.dist-info/WHEEL +5 -0
- mcpc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- mcpc_cli-0.1.0.dist-info/top_level.txt +1 -0
mcpc/__init__.py
ADDED
mcpc/__main__.py
ADDED
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
|