dwf-platform-cli 0.2.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.
Files changed (47) hide show
  1. dwf_cli/__init__.py +3 -0
  2. dwf_cli/__main__.py +4 -0
  3. dwf_cli/api/__init__.py +3 -0
  4. dwf_cli/api/auth.py +46 -0
  5. dwf_cli/api/client.py +113 -0
  6. dwf_cli/api/datamodel.py +458 -0
  7. dwf_cli/api/formmodel.py +197 -0
  8. dwf_cli/api/funcmodel.py +436 -0
  9. dwf_cli/cli/__init__.py +69 -0
  10. dwf_cli/cli/_common.py +152 -0
  11. dwf_cli/cli/auth.py +197 -0
  12. dwf_cli/cli/config.py +200 -0
  13. dwf_cli/cli/datamodel.py +2270 -0
  14. dwf_cli/cli/formmodel.py +1007 -0
  15. dwf_cli/cli/funcmodel.py +2055 -0
  16. dwf_cli/cli/schema.py +210 -0
  17. dwf_cli/core/__init__.py +0 -0
  18. dwf_cli/core/config.py +177 -0
  19. dwf_cli/core/crypto.py +22 -0
  20. dwf_cli/core/errors.py +63 -0
  21. dwf_cli/core/output.py +6 -0
  22. dwf_cli/core/validator.py +129 -0
  23. dwf_cli/mcp/__init__.py +3 -0
  24. dwf_cli/mcp/server.py +411 -0
  25. dwf_cli/schemas/__init__.py +37 -0
  26. dwf_cli/schemas/datamodel/attribute_bind.schema.json +21 -0
  27. dwf_cli/schemas/datamodel/attribute_create.schema.json +24 -0
  28. dwf_cli/schemas/datamodel/attribute_update.schema.json +21 -0
  29. dwf_cli/schemas/datamodel/create.schema.json +27 -0
  30. dwf_cli/schemas/datamodel/excel_confirm.schema.json +65 -0
  31. dwf_cli/schemas/datamodel/external_create.schema.json +47 -0
  32. dwf_cli/schemas/datamodel/external_update.schema.json +47 -0
  33. dwf_cli/schemas/datamodel/object_create.schema.json +24 -0
  34. dwf_cli/schemas/datamodel/object_update.schema.json +17 -0
  35. dwf_cli/schemas/datamodel/relation_create.schema.json +34 -0
  36. dwf_cli/schemas/datamodel/relation_update.schema.json +34 -0
  37. dwf_cli/schemas/datamodel/update.schema.json +26 -0
  38. dwf_cli/schemas/funcmodel/app_create.schema.json +150 -0
  39. dwf_cli/schemas/funcmodel/app_update.schema.json +153 -0
  40. dwf_cli/schemas/funcmodel/language-package_create.schema.json +15 -0
  41. dwf_cli/schemas/funcmodel/operations_create.schema.json +77 -0
  42. dwf_cli/schemas/funcmodel/operations_update.schema.json +76 -0
  43. dwf_platform_cli-0.2.0.dist-info/METADATA +347 -0
  44. dwf_platform_cli-0.2.0.dist-info/RECORD +47 -0
  45. dwf_platform_cli-0.2.0.dist-info/WHEEL +4 -0
  46. dwf_platform_cli-0.2.0.dist-info/entry_points.txt +3 -0
  47. dwf_platform_cli-0.2.0.dist-info/licenses/LICENSE +190 -0
dwf_cli/cli/schema.py ADDED
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import json as json_mod
5
+ import typing
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+
10
+ from dwf_cli.cli._common import handle_error
11
+ from dwf_cli.core.errors import DWFError
12
+ from dwf_cli.schemas import lookup
13
+
14
+ _HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
15
+
16
+ app = typer.Typer(
17
+ help="Show command schemas for agent consumption.",
18
+ context_settings=_HELP_CONTEXT_SETTINGS,
19
+ )
20
+
21
+ _SCHEMA_VERSION = "1.0"
22
+
23
+
24
+ @app.callback(invoke_without_command=True)
25
+ def schema_cmd(
26
+ ctx: typer.Context,
27
+ args: Annotated[
28
+ list[str] | None,
29
+ typer.Argument(help="Command path, e.g. 'datamodel create'"),
30
+ ] = None,
31
+ all_flag: Annotated[
32
+ bool,
33
+ typer.Option("--all", help="Show all command schemas"),
34
+ ] = False,
35
+ ) -> None:
36
+ try:
37
+ if all_flag or (not args):
38
+ _output_all()
39
+ return
40
+
41
+ command_path = " ".join(args)
42
+ _output_single(command_path)
43
+ except DWFError as exc:
44
+ handle_error(exc)
45
+
46
+
47
+ def _output_all() -> None:
48
+ root_app = _find_root_app()
49
+ if root_app is None:
50
+ typer.echo(json_mod.dumps({"schema_version": _SCHEMA_VERSION, "commands": []}))
51
+ raise typer.Exit(code=0)
52
+
53
+ commands = _flatten_commands(root_app)
54
+ typer.echo(
55
+ json_mod.dumps(
56
+ {"schema_version": _SCHEMA_VERSION, "commands": commands},
57
+ ensure_ascii=False,
58
+ indent=2,
59
+ )
60
+ )
61
+ raise typer.Exit(code=0)
62
+
63
+
64
+ def _output_single(command_path: str) -> None:
65
+ root_app = _find_root_app()
66
+ if root_app is None:
67
+ raise DWFError(f"Command not found: {command_path}")
68
+
69
+ info = _find_command(root_app, command_path)
70
+ if info is None:
71
+ raise DWFError(
72
+ f"Command not found: {command_path}. "
73
+ "Run 'dwf-cli schema --all' to see all available commands",
74
+ )
75
+
76
+ typer.echo(json_mod.dumps(info, ensure_ascii=False, indent=2))
77
+ raise typer.Exit(code=0)
78
+
79
+
80
+ def _find_root_app() -> typer.Typer | None:
81
+ import dwf_cli.cli as cli_mod
82
+
83
+ return getattr(cli_mod, "app", None)
84
+
85
+
86
+ def _flatten_commands(typer_app: typer.Typer, prefix: str = "") -> list[dict[str, Any]]:
87
+ results: list[dict[str, Any]] = []
88
+
89
+ for cmd in typer_app.registered_commands:
90
+ cmd_name = cmd.name or cmd.callback.__name__ if cmd.callback else None
91
+ if cmd_name is None:
92
+ continue
93
+ full_path = f"{prefix} {cmd_name}".strip() if prefix else cmd_name
94
+ info = _build_command_info(cmd, full_path)
95
+ results.append(info)
96
+
97
+ for group in typer_app.registered_groups:
98
+ group_name = group.name
99
+ if group_name is None:
100
+ continue
101
+ sub_app = group.typer_instance
102
+ if sub_app is not None:
103
+ group_prefix = f"{prefix} {group_name}".strip() if prefix else group_name
104
+ results.extend(_flatten_commands(sub_app, prefix=group_prefix))
105
+
106
+ return results
107
+
108
+
109
+ def _find_command(
110
+ typer_app: typer.Typer,
111
+ command_path: str,
112
+ full_path: str = "",
113
+ ) -> dict[str, Any] | None:
114
+ parts = command_path.split()
115
+ resolved = full_path or command_path
116
+
117
+ if len(parts) == 1:
118
+ for cmd in typer_app.registered_commands:
119
+ cmd_name = cmd.name or cmd.callback.__name__ if cmd.callback else None
120
+ if cmd_name == parts[0]:
121
+ return _build_command_info(cmd, resolved)
122
+
123
+ for group in typer_app.registered_groups:
124
+ group_name = group.name
125
+ if group_name is None or group_name == "schema":
126
+ continue
127
+ if parts[0] == group_name:
128
+ sub_app = group.typer_instance
129
+ if sub_app is not None:
130
+ remaining = " ".join(parts[1:])
131
+ return _find_command(sub_app, remaining, full_path=resolved)
132
+
133
+ return None
134
+
135
+
136
+ def _build_command_info(
137
+ cmd: typer.models.CommandInfo,
138
+ full_path: str,
139
+ ) -> dict[str, Any]:
140
+ info: dict[str, Any] = {"command": full_path}
141
+
142
+ if cmd.help:
143
+ info["help"] = cmd.help
144
+
145
+ params = _extract_params(cmd)
146
+ if params:
147
+ info["params"] = params
148
+
149
+ body = lookup(full_path)
150
+ if body:
151
+ info["body"] = body
152
+
153
+ return info
154
+
155
+
156
+ def _extract_params(cmd: typer.models.CommandInfo) -> list[dict[str, Any]]:
157
+ params: list[dict[str, Any]] = []
158
+ if cmd.callback is None:
159
+ return params
160
+
161
+ try:
162
+ hints = typing.get_type_hints(cmd.callback, include_extras=True)
163
+ except Exception:
164
+ hints = {}
165
+ sig = inspect.signature(cmd.callback)
166
+
167
+ for pname, p in sig.parameters.items():
168
+ hint = hints.get(pname)
169
+ if hint is None or not hasattr(hint, "__metadata__"):
170
+ continue
171
+
172
+ for meta in hint.__metadata__:
173
+ if isinstance(meta, typer.models.OptionInfo):
174
+ pi: dict[str, Any] = {"name": pname, "kind": "option"}
175
+ if meta.param_decls:
176
+ long_names = [d for d in meta.param_decls if d.startswith("--")]
177
+ short_names = [
178
+ d
179
+ for d in meta.param_decls
180
+ if d.startswith("-") and not d.startswith("--")
181
+ ]
182
+ if long_names:
183
+ pi["option"] = long_names[0]
184
+ elif meta.param_decls:
185
+ pi["option"] = meta.param_decls[0]
186
+ if short_names:
187
+ pi["short"] = short_names[0]
188
+ pi["help"] = meta.help or ""
189
+ has_default = p.default is not inspect.Parameter.empty
190
+ pi["required"] = not has_default
191
+ if has_default:
192
+ pi["default"] = p.default
193
+ args = getattr(hint, "__args__", None)
194
+ if args:
195
+ base = args[0]
196
+ if hasattr(base, "__members__"):
197
+ pi["enum"] = list(base.__members__.keys())
198
+ params.append(pi)
199
+ break
200
+ if isinstance(meta, typer.models.ArgumentInfo):
201
+ pi = {
202
+ "name": pname,
203
+ "kind": "argument",
204
+ "required": True,
205
+ "help": meta.help or "",
206
+ }
207
+ params.append(pi)
208
+ break
209
+
210
+ return params
File without changes
dwf_cli/core/config.py ADDED
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+
9
+ import typer
10
+
11
+ from dwf_cli.core.crypto import DEFAULT_AES_KEY, DEFAULT_AES_IV
12
+ from dwf_cli.core.errors import ConfigError
13
+
14
+
15
+ def parse_config_js(content: str, source_url: str | None = None) -> dict[str, str]:
16
+ result: dict[str, str] = {}
17
+
18
+ _FIELD_MAP: dict[str, str] = {
19
+ "modelerApiBase": "server",
20
+ "appApiBase": "app_server",
21
+ "aesKeyB": "aes_key",
22
+ "aesIv": "aes_iv",
23
+ }
24
+
25
+ source_parts = None
26
+ if source_url:
27
+ parsed = urlparse(source_url)
28
+ source_parts = {
29
+ "protocol": parsed.scheme,
30
+ "ip": parsed.hostname or "",
31
+ "port": str(parsed.port or ""),
32
+ }
33
+
34
+ for js_key, config_key in _FIELD_MAP.items():
35
+ pattern = rf"{js_key}\s*:\s*`([^`]*)`"
36
+ m = re.search(pattern, content)
37
+ if not m:
38
+ pattern2 = rf"{js_key}\s*:\s*'([^']*)'"
39
+ m = re.search(pattern2, content)
40
+ if not m:
41
+ continue
42
+ raw = m.group(1)
43
+ if source_parts:
44
+ proto = source_parts["protocol"]
45
+ raw = raw.replace("${protocol}", f"{proto}:")
46
+ raw = raw.replace("${ip}", source_parts["ip"])
47
+ raw = raw.replace("${port}", source_parts["port"])
48
+ if raw:
49
+ result[config_key] = raw
50
+
51
+ return result
52
+
53
+
54
+ def resolve_config_js_url(raw_input: str) -> str:
55
+ raw = raw_input.strip()
56
+ if (
57
+ re.match(r"^[A-Za-z]:\\", raw)
58
+ or raw.startswith("/")
59
+ or raw.startswith("./")
60
+ or raw.startswith("..\\")
61
+ or (not raw.startswith(("http://", "https://")) and Path(raw).exists())
62
+ ):
63
+ return raw
64
+ if not raw.startswith(("http://", "https://")):
65
+ raw = "http://" + raw
66
+ if raw.endswith("/config.js"):
67
+ return raw
68
+ for suffix in ("/modeler-api", "/app-api", "/monitor-api"):
69
+ if raw.endswith(suffix):
70
+ return raw[: -len(suffix)] + "/config.js"
71
+ if re.search(r"/modeler-web/?$", raw):
72
+ return raw.rstrip("/") + "/config.js"
73
+ return raw.rstrip("/") + "/modeler-web/config.js"
74
+
75
+
76
+ class Config:
77
+ _DIR: Path = Path(typer.get_app_dir("dwf-cli"))
78
+ _FILE: Path = _DIR / "config.json"
79
+
80
+ _URL_KEYS = {"server", "app_server"}
81
+
82
+ def __init__(self) -> None:
83
+ self._data: dict[str, object] = {}
84
+ self._DIR.mkdir(parents=True, exist_ok=True)
85
+ if self._FILE.exists():
86
+ try:
87
+ self._data = json.loads(self._FILE.read_text(encoding="utf-8"))
88
+ except (json.JSONDecodeError, OSError) as exc:
89
+ raise ConfigError(f"Failed to read config: {exc}") from exc
90
+
91
+ @property
92
+ def server(self) -> str | None:
93
+ value = self._data.get("server")
94
+ return str(value) if value is not None else None
95
+
96
+ @property
97
+ def app_server(self) -> str | None:
98
+ if "app_server" in self._data:
99
+ return str(self._data["app_server"])
100
+ return self.server
101
+
102
+ @property
103
+ def token(self) -> str | None:
104
+ value = self._data.get("token")
105
+ return str(value) if value is not None else None
106
+
107
+ @property
108
+ def aes_key(self) -> str:
109
+ return str(self._data.get("aes_key", DEFAULT_AES_KEY))
110
+
111
+ @property
112
+ def aes_iv(self) -> str:
113
+ return str(self._data.get("aes_iv", DEFAULT_AES_IV))
114
+
115
+ def save_token(self, token: str) -> None:
116
+ self._data["token"] = token
117
+ self._save()
118
+
119
+ def clear_token(self) -> None:
120
+ self._data.pop("token", None)
121
+ self._save()
122
+
123
+ _BLOCKED_PROTOCOLS = ("javascript:", "file:", "data:", "vbscript:")
124
+
125
+ def set(self, key: str, value: str) -> None:
126
+ if key in self._URL_KEYS:
127
+ self._validate_server_url(value)
128
+ self._data[key] = value
129
+ self._save()
130
+
131
+ @classmethod
132
+ def _validate_server_url(cls, url: str) -> None:
133
+ lower = url.strip().lower()
134
+ for proto in cls._BLOCKED_PROTOCOLS:
135
+ if lower.startswith(proto):
136
+ raise ConfigError(
137
+ f"Blocked protocol '{proto.rstrip(':')}' in server URL",
138
+ detail=f"server={url}",
139
+ )
140
+ if "://" in lower and not lower.startswith(("http://", "https://")):
141
+ raise ConfigError(
142
+ "Only http/https protocols allowed for server URL",
143
+ detail=f"server={url}",
144
+ )
145
+
146
+ def get(self, key: str) -> str | None:
147
+ value = self._data.get(key)
148
+ return str(value) if value is not None else None
149
+
150
+ def bulk_set(self, items: dict[str, str]) -> list[str]:
151
+ updated: list[str] = []
152
+ for key, value in items.items():
153
+ if key in self._URL_KEYS:
154
+ self._validate_server_url(value)
155
+ self._data[key] = value
156
+ updated.append(key)
157
+ if updated:
158
+ self._save()
159
+ return updated
160
+
161
+ def list_all(self) -> dict[str, Any]:
162
+ safe: dict[str, Any] = {}
163
+ for k, v in self._data.items():
164
+ if k == "token":
165
+ safe[k] = "***"
166
+ else:
167
+ safe[k] = v
168
+ return safe
169
+
170
+ def _save(self) -> None:
171
+ try:
172
+ self._FILE.write_text(
173
+ json.dumps(self._data, indent=2, ensure_ascii=False),
174
+ encoding="utf-8",
175
+ )
176
+ except OSError as exc:
177
+ raise ConfigError(f"Failed to write config: {exc}") from exc
dwf_cli/core/crypto.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import random
5
+ import string
6
+
7
+ from Crypto.Cipher import AES
8
+ from Crypto.Util.Padding import pad
9
+
10
+ DEFAULT_AES_KEY = "vndKmU#KbOxpifI0"
11
+ DEFAULT_AES_IV = "KPXUGHNBRWLYKRTE"
12
+
13
+
14
+ def encrypt_password(raw_password: str, key: str, iv: str) -> str:
15
+ cipher = AES.new(key.encode("utf-8"), AES.MODE_CBC, iv.encode("utf-8"))
16
+ padded = pad(raw_password.encode("utf-8"), AES.block_size)
17
+ ciphertext = cipher.encrypt(padded)
18
+ b64_once = base64.b64encode(ciphertext).decode("utf-8")
19
+ b64_twice = base64.b64encode(b64_once.encode("utf-8")).decode("utf-8")
20
+ prefix = "".join(random.choices(string.ascii_letters + string.digits, k=3))
21
+ suffix = "".join(random.choices(string.ascii_letters + string.digits, k=5))
22
+ return f"{prefix}{b64_twice}{suffix}"
dwf_cli/core/errors.py ADDED
@@ -0,0 +1,63 @@
1
+ _RETRYABLE_STATUS = {408, 429, 502, 503, 504}
2
+
3
+
4
+ class ExitCode:
5
+ SUCCESS = 0
6
+ GENERAL = 1
7
+ AUTH = 2
8
+ NOT_FOUND = 3
9
+ API = 4
10
+ CONFLICT = 5
11
+ CONFIG = 6
12
+
13
+
14
+ class DWFError(Exception):
15
+ exit_code: int = ExitCode.GENERAL
16
+ retryable: bool = False
17
+ suggestion: str | None = None
18
+
19
+ def __init__(self, message: str, *, detail: str | None = None) -> None:
20
+ self.detail = detail
21
+ super().__init__(message)
22
+
23
+
24
+ class AuthError(DWFError):
25
+ exit_code = ExitCode.AUTH
26
+ retryable = False
27
+ suggestion = "检查登录状态,运行 dwf-cli auth login"
28
+
29
+
30
+ class NotFoundError(DWFError):
31
+ exit_code = ExitCode.NOT_FOUND
32
+ retryable = False
33
+ suggestion = "确认资源名称或 OID 是否正确"
34
+
35
+
36
+ class ConflictError(DWFError):
37
+ exit_code = ExitCode.CONFLICT
38
+ retryable = False
39
+ suggestion = "资源已存在,使用 --if-not-exists 跳过"
40
+
41
+ def __init__(self, message: str, *, detail: str | None = None) -> None:
42
+ self.detail = detail
43
+ super().__init__(message)
44
+
45
+
46
+ class APIError(DWFError):
47
+ exit_code = ExitCode.API
48
+ retryable = False
49
+ suggestion = "检查网络连接和服务器状态"
50
+
51
+ def __init__(self, status: int, message: str) -> None:
52
+ self.status = status
53
+ self.message = message
54
+ if status in _RETRYABLE_STATUS:
55
+ self.retryable = True
56
+ self.suggestion = "服务器暂时不可用,建议稍后重试"
57
+ super().__init__(f"API error {status}: {message}")
58
+
59
+
60
+ class ConfigError(DWFError):
61
+ exit_code = ExitCode.CONFIG
62
+ retryable = False
63
+ suggestion = "运行 dwf-cli config set 检查配置项"
dwf_cli/core/output.py ADDED
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class OutputFormat(str, Enum):
5
+ table = "table"
6
+ json = "json"
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ _TYPE_MAP: dict[str, type | tuple[type, ...]] = {
7
+ "string": str,
8
+ "integer": (int,),
9
+ "boolean": (bool,),
10
+ "number": (int, float),
11
+ "array": (list,),
12
+ "object": (dict,),
13
+ }
14
+
15
+
16
+ def validate_body(schema: dict[str, Any], data: Any) -> list[str]:
17
+ errors: list[str] = []
18
+
19
+ body_type = schema.get("type", "object")
20
+ if body_type == "array" and not isinstance(data, list):
21
+ errors.append(f"Expected array, got {type(data).__name__}")
22
+ return errors
23
+ if body_type == "object" and not isinstance(data, dict):
24
+ errors.append(f"Expected object, got {type(data).__name__}")
25
+ return errors
26
+
27
+ if schema.get("dynamic"):
28
+ return _validate_dynamic(schema, data, errors)
29
+
30
+ fields = schema.get("fields", [])
31
+ if not fields:
32
+ return errors
33
+
34
+ if body_type == "array":
35
+ if not data:
36
+ errors.append("Array must not be empty")
37
+ return errors
38
+ for i, item in enumerate(data):
39
+ item_errors = _validate_fields(fields, item, prefix=f"[{i}]")
40
+ errors.extend(item_errors)
41
+ else:
42
+ errors.extend(_validate_fields(fields, data))
43
+
44
+ return errors
45
+
46
+
47
+ def _validate_fields(
48
+ fields: list[dict[str, Any]],
49
+ data: dict[str, Any],
50
+ prefix: str = "",
51
+ ) -> list[str]:
52
+ errors: list[str] = []
53
+
54
+ for field in fields:
55
+ name = field["name"]
56
+ required = field.get("required", False)
57
+ path = f"{prefix}.{name}" if prefix else name
58
+
59
+ if required and name not in data:
60
+ errors.append(f"Missing required field: {path}")
61
+ continue
62
+
63
+ if name not in data:
64
+ continue
65
+
66
+ value = data[name]
67
+ value_errors = _validate_field_value(field, value, path)
68
+ errors.extend(value_errors)
69
+
70
+ return errors
71
+
72
+
73
+ def _validate_field_value(
74
+ field: dict[str, Any],
75
+ value: Any,
76
+ path: str,
77
+ ) -> list[str]:
78
+ errors: list[str] = []
79
+
80
+ expected_type = field.get("type")
81
+ if expected_type and expected_type in _TYPE_MAP:
82
+ py_type = _TYPE_MAP[expected_type]
83
+ if expected_type == "boolean" and isinstance(value, int):
84
+ errors.append(f"Field {path}: expected boolean, got int")
85
+ elif not isinstance(value, py_type):
86
+ errors.append(
87
+ f"Field {path}: expected {expected_type}, got {type(value).__name__}"
88
+ )
89
+ return errors
90
+
91
+ enum_values = field.get("enum")
92
+ if enum_values and value not in enum_values:
93
+ errors.append(f"Field {path}: value '{value}' not in enum {enum_values}")
94
+
95
+ if "item_schema" in field and isinstance(value, list):
96
+ item_fields = field["item_schema"].get("fields", [])
97
+ for i, item in enumerate(value):
98
+ if isinstance(item, dict):
99
+ item_errors = _validate_fields(item_fields, item, prefix=f"{path}[{i}]")
100
+ errors.extend(item_errors)
101
+
102
+ return errors
103
+
104
+
105
+ def _validate_dynamic(
106
+ schema: dict[str, Any],
107
+ data: Any,
108
+ errors: list[str],
109
+ ) -> list[str]:
110
+ fields = schema.get("fields", [])
111
+ if not fields:
112
+ return errors
113
+
114
+ if isinstance(data, list):
115
+ for i, item in enumerate(data):
116
+ if isinstance(item, dict):
117
+ for field in fields:
118
+ name = field["name"]
119
+ required = field.get("required", False)
120
+ if required and name not in item:
121
+ errors.append(f"Missing required field: [{i}].{name}")
122
+ elif isinstance(data, dict):
123
+ for field in fields:
124
+ name = field["name"]
125
+ required = field.get("required", False)
126
+ if required and name not in data:
127
+ errors.append(f"Missing required field: {name}")
128
+
129
+ return errors
@@ -0,0 +1,3 @@
1
+ from dwf_cli.mcp.server import mcp
2
+
3
+ __all__ = ["mcp"]