structurize 3.4.2__tar.gz → 3.5.0__tar.gz

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 (91) hide show
  1. {structurize-3.4.2/structurize.egg-info → structurize-3.5.0}/PKG-INFO +1 -1
  2. {structurize-3.4.2 → structurize-3.5.0}/avrotize/_version.py +3 -3
  3. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotojava.py +4 -3
  4. {structurize-3.4.2 → structurize-3.5.0}/avrotize/constants.py +17 -12
  5. {structurize-3.4.2 → structurize-3.5.0}/avrotize/dependencies/typescript/node22/package.json +1 -1
  6. structurize-3.5.0/avrotize/mcp_server.py +498 -0
  7. {structurize-3.4.2 → structurize-3.5.0/structurize.egg-info}/PKG-INFO +1 -1
  8. {structurize-3.4.2 → structurize-3.5.0}/structurize.egg-info/SOURCES.txt +1 -0
  9. structurize-3.4.2/avrotize/mcp_server.py +0 -253
  10. {structurize-3.4.2 → structurize-3.5.0}/.gitignore +0 -0
  11. {structurize-3.4.2 → structurize-3.5.0}/LICENSE +0 -0
  12. {structurize-3.4.2 → structurize-3.5.0}/MANIFEST.in +0 -0
  13. {structurize-3.4.2 → structurize-3.5.0}/README.md +0 -0
  14. {structurize-3.4.2 → structurize-3.5.0}/avrotize/__init__.py +0 -0
  15. {structurize-3.4.2 → structurize-3.5.0}/avrotize/__main__.py +0 -0
  16. {structurize-3.4.2 → structurize-3.5.0}/avrotize/asn1toavro.py +0 -0
  17. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotize.py +0 -0
  18. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotocpp.py +0 -0
  19. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotocsharp.py +0 -0
  20. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotocsv.py +0 -0
  21. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotodatapackage.py +0 -0
  22. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotodb.py +0 -0
  23. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotogo.py +0 -0
  24. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotographql.py +0 -0
  25. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotoiceberg.py +0 -0
  26. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotojs.py +0 -0
  27. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotojsons.py +0 -0
  28. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotojstruct.py +0 -0
  29. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotokusto.py +0 -0
  30. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotomd.py +0 -0
  31. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotools.py +0 -0
  32. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotoparquet.py +0 -0
  33. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotoproto.py +0 -0
  34. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotopython.py +0 -0
  35. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotorust.py +0 -0
  36. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotots.py +0 -0
  37. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrotoxsd.py +0 -0
  38. {structurize-3.4.2 → structurize-3.5.0}/avrotize/avrovalidator.py +0 -0
  39. {structurize-3.4.2 → structurize-3.5.0}/avrotize/cddltostructure.py +0 -0
  40. {structurize-3.4.2 → structurize-3.5.0}/avrotize/choice_inference.py +0 -0
  41. {structurize-3.4.2 → structurize-3.5.0}/avrotize/commands.json +0 -0
  42. {structurize-3.4.2 → structurize-3.5.0}/avrotize/common.py +0 -0
  43. {structurize-3.4.2 → structurize-3.5.0}/avrotize/csvtoavro.py +0 -0
  44. {structurize-3.4.2 → structurize-3.5.0}/avrotize/datapackagetoavro.py +0 -0
  45. {structurize-3.4.2 → structurize-3.5.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  46. {structurize-3.4.2 → structurize-3.5.0}/avrotize/dependency_resolver.py +0 -0
  47. {structurize-3.4.2 → structurize-3.5.0}/avrotize/dependency_version.py +0 -0
  48. {structurize-3.4.2 → structurize-3.5.0}/avrotize/jsonstoavro.py +0 -0
  49. {structurize-3.4.2 → structurize-3.5.0}/avrotize/jsonstostructure.py +0 -0
  50. {structurize-3.4.2 → structurize-3.5.0}/avrotize/jsontoschema.py +0 -0
  51. {structurize-3.4.2 → structurize-3.5.0}/avrotize/jstructtoavro.py +0 -0
  52. {structurize-3.4.2 → structurize-3.5.0}/avrotize/kstructtoavro.py +0 -0
  53. {structurize-3.4.2 → structurize-3.5.0}/avrotize/kustotoavro.py +0 -0
  54. {structurize-3.4.2 → structurize-3.5.0}/avrotize/kustotojstruct.py +0 -0
  55. {structurize-3.4.2 → structurize-3.5.0}/avrotize/openapitostructure.py +0 -0
  56. {structurize-3.4.2 → structurize-3.5.0}/avrotize/parquettoavro.py +0 -0
  57. {structurize-3.4.2 → structurize-3.5.0}/avrotize/proto2parser.py +0 -0
  58. {structurize-3.4.2 → structurize-3.5.0}/avrotize/proto3parser.py +0 -0
  59. {structurize-3.4.2 → structurize-3.5.0}/avrotize/prototoavro.py +0 -0
  60. {structurize-3.4.2 → structurize-3.5.0}/avrotize/schema_inference.py +0 -0
  61. {structurize-3.4.2 → structurize-3.5.0}/avrotize/sqltoavro.py +0 -0
  62. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretocddl.py +0 -0
  63. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretocpp.py +0 -0
  64. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretocsharp.py +0 -0
  65. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretocsv.py +0 -0
  66. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretodatapackage.py +0 -0
  67. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretodb.py +0 -0
  68. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretogo.py +0 -0
  69. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretographql.py +0 -0
  70. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretoiceberg.py +0 -0
  71. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretojava.py +0 -0
  72. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretojs.py +0 -0
  73. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretojsons.py +0 -0
  74. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretokusto.py +0 -0
  75. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretomd.py +0 -0
  76. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretoproto.py +0 -0
  77. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretopython.py +0 -0
  78. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretorust.py +0 -0
  79. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretots.py +0 -0
  80. {structurize-3.4.2 → structurize-3.5.0}/avrotize/structuretoxsd.py +0 -0
  81. {structurize-3.4.2 → structurize-3.5.0}/avrotize/validate.py +0 -0
  82. {structurize-3.4.2 → structurize-3.5.0}/avrotize/xmltoschema.py +0 -0
  83. {structurize-3.4.2 → structurize-3.5.0}/avrotize/xsdtoavro.py +0 -0
  84. {structurize-3.4.2 → structurize-3.5.0}/build.ps1 +0 -0
  85. {structurize-3.4.2 → structurize-3.5.0}/build.sh +0 -0
  86. {structurize-3.4.2 → structurize-3.5.0}/pyproject.toml +0 -0
  87. {structurize-3.4.2 → structurize-3.5.0}/setup.cfg +0 -0
  88. {structurize-3.4.2 → structurize-3.5.0}/structurize.egg-info/dependency_links.txt +0 -0
  89. {structurize-3.4.2 → structurize-3.5.0}/structurize.egg-info/entry_points.txt +0 -0
  90. {structurize-3.4.2 → structurize-3.5.0}/structurize.egg-info/requires.txt +0 -0
  91. {structurize-3.4.2 → structurize-3.5.0}/structurize.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structurize
3
- Version: 3.4.2
3
+ Version: 3.5.0
4
4
  Summary: Tools to convert from and to JSON Structure from various other schema languages.
5
5
  Author-email: Clemens Vasters <clemensv@microsoft.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '3.4.2'
32
- __version_tuple__ = version_tuple = (3, 4, 2)
31
+ __version__ = version = '3.5.0'
32
+ __version_tuple__ = version_tuple = (3, 5, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g6daf45a76'
34
+ __commit_id__ = commit_id = 'g95f637e11'
@@ -4,8 +4,8 @@
4
4
  import json
5
5
  import os
6
6
  from typing import Dict, List, Tuple, Union
7
- from avrotize.constants import (AVRO_VERSION, JACKSON_VERSION, JDK_VERSION,
8
- JUNIT_VERSION, MAVEN_COMPILER_VERSION, MAVEN_SUREFIRE_VERSION)
7
+ from avrotize.constants import (AVRO_VERSION, JACKSON_ANNOTATIONS_VERSION, JACKSON_VERSION,
8
+ JDK_VERSION, JUNIT_VERSION, MAVEN_COMPILER_VERSION, MAVEN_SUREFIRE_VERSION)
9
9
 
10
10
  from avrotize.common import pascal, camel, is_generic_avro_type, inline_avro_references, build_flat_type_dict
11
11
 
@@ -42,7 +42,7 @@ POM_CONTENT = """<?xml version="1.0" encoding="UTF-8"?>
42
42
  <dependency>
43
43
  <groupId>com.fasterxml.jackson.core</groupId>
44
44
  <artifactId>jackson-annotations</artifactId>
45
- <version>{JACKSON_VERSION}</version>
45
+ <version>{JACKSON_ANNOTATIONS_VERSION}</version>
46
46
  </dependency>
47
47
  <dependency>
48
48
  <groupId>org.junit.jupiter</groupId>
@@ -2160,6 +2160,7 @@ class AvroToJava:
2160
2160
  artifactid=artifactid,
2161
2161
  AVRO_VERSION=AVRO_VERSION,
2162
2162
  JACKSON_VERSION=JACKSON_VERSION,
2163
+ JACKSON_ANNOTATIONS_VERSION=JACKSON_ANNOTATIONS_VERSION,
2163
2164
  JDK_VERSION=JDK_VERSION,
2164
2165
  JUNIT_VERSION=JUNIT_VERSION,
2165
2166
  MAVEN_COMPILER_VERSION=MAVEN_COMPILER_VERSION,
@@ -17,61 +17,66 @@ try:
17
17
  except ValueError:
18
18
  JACKSON_VERSION = '2.18.2' # Fallback
19
19
 
20
+ try:
21
+ JACKSON_ANNOTATIONS_VERSION = get_dependency_version('java', 'jdk21', 'com.fasterxml.jackson.core:jackson-annotations')
22
+ except ValueError:
23
+ JACKSON_ANNOTATIONS_VERSION = '2.18.2' # Fallback
24
+
20
25
  JDK_VERSION = '21'
21
26
 
22
27
  # C# dependencies
23
28
  try:
24
- CSHARP_AVRO_VERSION = get_dependency_version('cs', 'net90', 'Apache.Avro')
29
+ CSHARP_AVRO_VERSION = get_dependency_version('cs', 'net100', 'Apache.Avro')
25
30
  except ValueError:
26
31
  CSHARP_AVRO_VERSION = '1.12.0'
27
32
 
28
33
  try:
29
- NEWTONSOFT_JSON_VERSION = get_dependency_version('cs', 'net90', 'Newtonsoft.Json')
34
+ NEWTONSOFT_JSON_VERSION = get_dependency_version('cs', 'net100', 'Newtonsoft.Json')
30
35
  except ValueError:
31
36
  NEWTONSOFT_JSON_VERSION = '13.0.3'
32
37
 
33
38
  try:
34
- SYSTEM_TEXT_JSON_VERSION = get_dependency_version('cs', 'net90', 'System.Text.Json')
39
+ SYSTEM_TEXT_JSON_VERSION = get_dependency_version('cs', 'net100', 'System.Text.Json')
35
40
  except ValueError:
36
41
  SYSTEM_TEXT_JSON_VERSION = '9.0.3'
37
42
 
38
43
  try:
39
- SYSTEM_MEMORY_DATA_VERSION = get_dependency_version('cs', 'net90', 'System.Memory.Data')
44
+ SYSTEM_MEMORY_DATA_VERSION = get_dependency_version('cs', 'net100', 'System.Memory.Data')
40
45
  except ValueError:
41
46
  SYSTEM_MEMORY_DATA_VERSION = '9.0.3'
42
47
 
43
48
  try:
44
- PROTOBUF_NET_VERSION = get_dependency_version('cs', 'net90', 'protobuf-net')
49
+ PROTOBUF_NET_VERSION = get_dependency_version('cs', 'net100', 'protobuf-net')
45
50
  except ValueError:
46
51
  PROTOBUF_NET_VERSION = '3.2.30'
47
52
 
48
53
  try:
49
- NUNIT_VERSION = get_dependency_version('cs', 'net90', 'NUnit')
54
+ NUNIT_VERSION = get_dependency_version('cs', 'net100', 'NUnit')
50
55
  except ValueError:
51
56
  NUNIT_VERSION = '4.3.2'
52
57
 
53
58
  try:
54
- NUNIT_ADAPTER_VERSION = get_dependency_version('cs', 'net90', 'NUnit3TestAdapter')
59
+ NUNIT_ADAPTER_VERSION = get_dependency_version('cs', 'net100', 'NUnit3TestAdapter')
55
60
  except ValueError:
56
61
  NUNIT_ADAPTER_VERSION = '5.0.0'
57
62
 
58
63
  try:
59
- MSTEST_SDK_VERSION = get_dependency_version('cs', 'net90', 'Microsoft.NET.Test.Sdk')
64
+ MSTEST_SDK_VERSION = get_dependency_version('cs', 'net100', 'Microsoft.NET.Test.Sdk')
60
65
  except ValueError:
61
66
  MSTEST_SDK_VERSION = '17.13.0'
62
67
 
63
68
  try:
64
- COVERLET_VERSION = get_dependency_version('cs', 'net90', 'coverlet.collector')
69
+ COVERLET_VERSION = get_dependency_version('cs', 'net100', 'coverlet.collector')
65
70
  except ValueError:
66
71
  COVERLET_VERSION = '6.0.4'
67
72
 
68
73
  try:
69
- MSGPACK_VERSION = get_dependency_version('cs', 'net90', 'MessagePack')
74
+ MSGPACK_VERSION = get_dependency_version('cs', 'net100', 'MessagePack')
70
75
  except ValueError:
71
76
  MSGPACK_VERSION = '2.5.187'
72
77
 
73
78
  try:
74
- CBOR_VERSION = get_dependency_version('cs', 'net90', 'Dahomey.Cbor')
79
+ CBOR_VERSION = get_dependency_version('cs', 'net100', 'Dahomey.Cbor')
75
80
  except ValueError:
76
81
  CBOR_VERSION = '1.25.1'
77
82
 
@@ -89,4 +94,4 @@ except ValueError:
89
94
  try:
90
95
  MAVEN_COMPILER_VERSION = get_dependency_version('java', 'jdk21', 'org.apache.maven.plugins:maven-compiler-plugin')
91
96
  except ValueError:
92
- MAVEN_COMPILER_VERSION = '3.13.0'
97
+ MAVEN_COMPILER_VERSION = '3.13.0'
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@types/node": "^25.0.3",
12
- "typescript": "^5.7.2",
12
+ "typescript": "^6.0.2",
13
13
  "jest": "^30.2.0",
14
14
  "@types/jest": "^30.0.0"
15
15
  }
@@ -0,0 +1,498 @@
1
+ """MCP server integration for Avrotize conversions.
2
+
3
+ Implements the MCP (Model Context Protocol) stdio transport directly via
4
+ JSON-RPC 2.0 over stdin/stdout to avoid the heavy ``mcp`` library import
5
+ (~2-3 s for uvicorn, starlette, httpx, pydantic). The ``mcp`` library
6
+ is used only as a fallback when *explicitly* requested.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json as _json
13
+ import os
14
+ import sys
15
+ import tempfile
16
+ from typing import Any, Dict, List
17
+
18
+ from avrotize.avrotize import dynamic_import, load_commands
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # MCP protocol version supported by this server
22
+ # ---------------------------------------------------------------------------
23
+ _MCP_PROTOCOL_VERSION = "2025-03-26"
24
+
25
+
26
+ def _command_dest(arg: Dict[str, Any]) -> str:
27
+ name = arg["name"]
28
+ if name.startswith("-"):
29
+ return arg.get("dest", name.lstrip("-").replace("-", "_").replace(".", "_"))
30
+ return name
31
+
32
+
33
+ def _coerce_value(value: Any, arg_type: str) -> Any:
34
+ if value is None:
35
+ return None
36
+ if arg_type == "bool":
37
+ if isinstance(value, bool):
38
+ return value
39
+ if isinstance(value, (int, float)):
40
+ return bool(value)
41
+ sval = str(value).strip().lower()
42
+ return sval in {"1", "true", "yes", "y", "on"}
43
+ if arg_type == "int":
44
+ return int(value)
45
+ if arg_type == "float":
46
+ return float(value)
47
+ return str(value) if arg_type == "str" else value
48
+
49
+
50
+ def _build_namespace(command: Dict[str, Any], options: Dict[str, Any]) -> argparse.Namespace:
51
+ namespace = argparse.Namespace()
52
+ normalized_options = {
53
+ key.lstrip("-").replace("-", "_"): value
54
+ for key, value in (options or {}).items()
55
+ }
56
+
57
+ for arg in command.get("args", []):
58
+ dest = _command_dest(arg)
59
+ default_value = arg.get("default", False if arg.get("type") == "bool" else None)
60
+ setattr(namespace, dest, default_value)
61
+ if dest in normalized_options:
62
+ raw_value = normalized_options[dest]
63
+ if arg.get("nargs") in {"+", "*"}:
64
+ values = raw_value if isinstance(raw_value, list) else [raw_value]
65
+ setattr(namespace, dest, [_coerce_value(v, arg["type"]) for v in values])
66
+ else:
67
+ setattr(namespace, dest, _coerce_value(raw_value, arg["type"]))
68
+
69
+ return namespace
70
+
71
+
72
+ def _resolve_input_path(command_args: argparse.Namespace, explicit_input_path: str | None) -> str | None:
73
+ if explicit_input_path:
74
+ return explicit_input_path
75
+ return (
76
+ getattr(command_args, "input", None)
77
+ or getattr(command_args, "avsc", None)
78
+ or getattr(command_args, "proto", None)
79
+ or getattr(command_args, "jsons", None)
80
+ or getattr(command_args, "xsd", None)
81
+ or getattr(command_args, "kusto_uri", None)
82
+ or getattr(command_args, "parquet", None)
83
+ or getattr(command_args, "asn", None)
84
+ or getattr(command_args, "kstruct", None)
85
+ )
86
+
87
+
88
+ def _find_primary_input_arg(command: Dict[str, Any]) -> Dict[str, Any] | None:
89
+ return next(
90
+ (arg for arg in command.get("args", []) if isinstance(arg.get("name"), str) and not arg["name"].startswith("-")),
91
+ None,
92
+ )
93
+
94
+
95
+ def _inject_input_into_namespace(command: Dict[str, Any], command_args: argparse.Namespace, input_value: str) -> None:
96
+ primary_input_arg = _find_primary_input_arg(command)
97
+ if not primary_input_arg:
98
+ return
99
+
100
+ dest = _command_dest(primary_input_arg)
101
+ current_value = getattr(command_args, dest, None)
102
+
103
+ if primary_input_arg.get("nargs") in {"+", "*"}:
104
+ if current_value in (None, "", []):
105
+ setattr(command_args, dest, [input_value])
106
+ elif isinstance(current_value, list) and input_value not in current_value:
107
+ setattr(command_args, dest, [input_value, *current_value])
108
+ elif not isinstance(current_value, list):
109
+ setattr(command_args, dest, [input_value, current_value])
110
+ else:
111
+ if current_value in (None, ""):
112
+ setattr(command_args, dest, input_value)
113
+
114
+
115
+ def _find_command(command_name: str) -> Dict[str, Any] | None:
116
+ return next((cmd for cmd in load_commands() if cmd.get("command") == command_name), None)
117
+
118
+
119
+ def _list_commands() -> List[Dict[str, Any]]:
120
+ commands = load_commands()
121
+ result: List[Dict[str, Any]] = []
122
+ for command in commands:
123
+ if command.get("command") == "mcp":
124
+ continue
125
+ result.append(
126
+ {
127
+ "command": command.get("command"),
128
+ "description": command.get("description"),
129
+ "group": command.get("group"),
130
+ "args": [arg.get("name") for arg in command.get("args", [])],
131
+ }
132
+ )
133
+ return result
134
+
135
+
136
+ def _describe_capabilities() -> Dict[str, Any]:
137
+ commands = load_commands()
138
+ command_names = [cmd.get("command") for cmd in commands if cmd.get("command") != "mcp"]
139
+ return {
140
+ "server": "avrotize",
141
+ "purpose": "Schema conversion and schema-driven code generation",
142
+ "use_when": [
143
+ "Converting between schema formats (Avro, JSON Schema, Proto, XSD, Parquet, etc.)",
144
+ "Generating code from Avro/JSON Structure schemas (C#, Java, Python, TypeScript, JavaScript, Rust, Go, C++)",
145
+ "Inferring schemas from sample JSON/XML/CSV/parquet inputs",
146
+ ],
147
+ "do_not_use_when": [
148
+ "Task is unrelated to schema conversion or code generation",
149
+ "Task requires arbitrary code execution outside avrotize command set",
150
+ ],
151
+ "recommended_flow": [
152
+ "Call describe_capabilities for high-level routing",
153
+ "Call list_conversions to discover available commands",
154
+ "Call get_conversion(command) to inspect required args/options",
155
+ "Call run_conversion(...) to execute",
156
+ ],
157
+ "tools": {
158
+ "describe_capabilities": "High-level guidance for when and how to use this server",
159
+ "list_conversions": "List available conversion/code generation commands",
160
+ "get_conversion": "Inspect metadata and args for a specific command",
161
+ "run_conversion": "Execute a specific conversion command",
162
+ },
163
+ "command_count": len(command_names),
164
+ "commands": command_names,
165
+ }
166
+
167
+
168
+ def _execute_conversion(
169
+ command_name: str,
170
+ input_path: str | None = None,
171
+ input_content: str | None = None,
172
+ output_path: str | None = None,
173
+ options: Dict[str, Any] | None = None,
174
+ ) -> Dict[str, Any]:
175
+ command = _find_command(command_name)
176
+ if not command:
177
+ raise ValueError(f"Unknown command '{command_name}'.")
178
+
179
+ command_args = _build_namespace(command, options or {})
180
+ temp_input_path = None
181
+ temp_output_path = None
182
+
183
+ try:
184
+ resolved_input = _resolve_input_path(command_args, input_path)
185
+ skip_input_file_handling = command.get("skip_input_file_handling", False)
186
+ if input_content is not None and not resolved_input:
187
+ temp_input = tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8")
188
+ temp_input.write(input_content)
189
+ temp_input.flush()
190
+ temp_input.close()
191
+ temp_input_path = temp_input.name
192
+ resolved_input = temp_input_path
193
+
194
+ if not skip_input_file_handling and not resolved_input:
195
+ if input_content is None:
196
+ raise ValueError("This command requires input_path or input_content.")
197
+
198
+ if resolved_input:
199
+ _inject_input_into_namespace(command, command_args, resolved_input)
200
+
201
+ if not output_path and any(v == "output_file_path" for v in command.get("function", {}).get("args", {}).values()):
202
+ temp_output = tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8")
203
+ temp_output_path = temp_output.name
204
+ temp_output.close()
205
+ output_path = temp_output_path
206
+
207
+ module_name, func_name = command["function"]["name"].rsplit(".", 1)
208
+ func = dynamic_import(module_name, func_name)
209
+
210
+ func_args: Dict[str, Any] = {}
211
+ for arg_name, arg_value in command["function"]["args"].items():
212
+ if arg_value == "input_file_path":
213
+ func_args[arg_name] = resolved_input
214
+ elif arg_value == "output_file_path":
215
+ func_args[arg_name] = output_path
216
+ elif isinstance(arg_value, str) and arg_value.startswith("args."):
217
+ attr_name = arg_value[5:]
218
+ if hasattr(command_args, attr_name):
219
+ func_args[arg_name] = getattr(command_args, attr_name)
220
+ else:
221
+ func_args[arg_name] = arg_value
222
+
223
+ func(**func_args)
224
+
225
+ output_content = None
226
+ if temp_output_path and os.path.exists(temp_output_path):
227
+ with open(temp_output_path, "r", encoding="utf-8") as out_file:
228
+ output_content = out_file.read()
229
+
230
+ return {
231
+ "success": True,
232
+ "command": command_name,
233
+ "output_path": output_path,
234
+ "output_content": output_content,
235
+ }
236
+ finally:
237
+ if temp_input_path and os.path.exists(temp_input_path):
238
+ os.remove(temp_input_path)
239
+ if temp_output_path and os.path.exists(temp_output_path):
240
+ os.remove(temp_output_path)
241
+
242
+
243
+ def _tool_schemas() -> List[Dict[str, Any]]:
244
+ """Return MCP tool definitions for the four exposed tools."""
245
+ return [
246
+ {
247
+ "name": "describe_capabilities",
248
+ "description": "Describe when this server should be used and how to invoke it.",
249
+ "inputSchema": {"type": "object", "properties": {}},
250
+ },
251
+ {
252
+ "name": "list_conversions",
253
+ "description": "List available Avrotize conversion commands.",
254
+ "inputSchema": {"type": "object", "properties": {}},
255
+ },
256
+ {
257
+ "name": "get_conversion",
258
+ "description": "Get metadata for a specific conversion command.",
259
+ "inputSchema": {
260
+ "type": "object",
261
+ "properties": {
262
+ "command": {"type": "string", "description": "The conversion command name."},
263
+ },
264
+ "required": ["command"],
265
+ },
266
+ },
267
+ {
268
+ "name": "run_conversion",
269
+ "description": "Run a conversion command and return conversion output information.",
270
+ "inputSchema": {
271
+ "type": "object",
272
+ "properties": {
273
+ "command": {"type": "string", "description": "The conversion command name."},
274
+ "input_path": {"type": "string", "description": "Path to the input file.", "default": ""},
275
+ "input_content": {"type": "string", "description": "Inline input content.", "default": ""},
276
+ "output_path": {"type": "string", "description": "Path for the output file.", "default": ""},
277
+ "options": {
278
+ "type": "object",
279
+ "description": "Additional command options.",
280
+ "additionalProperties": True,
281
+ },
282
+ },
283
+ "required": ["command"],
284
+ },
285
+ },
286
+ ]
287
+
288
+
289
+ def _dispatch_tool(name: str, arguments: Dict[str, Any]) -> Any:
290
+ """Dispatch a tool call by *name* and return a Python object."""
291
+ if name == "describe_capabilities":
292
+ return _describe_capabilities()
293
+ if name == "list_conversions":
294
+ return _list_commands()
295
+ if name == "get_conversion":
296
+ return _get_conversion_handler(arguments)
297
+ if name == "run_conversion":
298
+ return _run_conversion_handler(arguments)
299
+ raise ValueError(f"Unknown tool '{name}'.")
300
+
301
+
302
+ def _get_conversion_handler(args: Dict[str, Any]) -> Dict[str, Any]:
303
+ command_name = args.get("command", "")
304
+ cmd = _find_command(command_name)
305
+ if not cmd:
306
+ raise ValueError(f"Unknown command '{command_name}'.")
307
+ return {
308
+ "command": cmd.get("command"),
309
+ "description": cmd.get("description"),
310
+ "group": cmd.get("group"),
311
+ "args": cmd.get("args", []),
312
+ }
313
+
314
+
315
+ def _run_conversion_handler(args: Dict[str, Any]) -> Dict[str, Any]:
316
+ return _execute_conversion(
317
+ command_name=args.get("command", ""),
318
+ input_path=args.get("input_path") or None,
319
+ input_content=args.get("input_content") or None,
320
+ output_path=args.get("output_path") or None,
321
+ options=args.get("options") or {},
322
+ )
323
+
324
+
325
+ # ---------------------------------------------------------------------------
326
+ # Lightweight JSON-RPC / MCP stdio transport
327
+ # ---------------------------------------------------------------------------
328
+
329
+ def _jsonrpc_error(req_id: Any, code: int, message: str) -> Dict[str, Any]:
330
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
331
+
332
+
333
+ def _jsonrpc_result(req_id: Any, result: Any) -> Dict[str, Any]:
334
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
335
+
336
+
337
+ def _handle_message(msg: Dict[str, Any]) -> Dict[str, Any] | None:
338
+ """Process one JSON-RPC message. Returns a response dict, or None for notifications."""
339
+ method = msg.get("method", "")
340
+ req_id = msg.get("id")
341
+ params = msg.get("params", {})
342
+
343
+ # --- Lifecycle ----------------------------------------------------------
344
+ if method == "initialize":
345
+ return _jsonrpc_result(req_id, {
346
+ "protocolVersion": _MCP_PROTOCOL_VERSION,
347
+ "capabilities": {"tools": {}},
348
+ "serverInfo": {"name": "avrotize", "version": _get_version()},
349
+ })
350
+
351
+ if method == "notifications/initialized":
352
+ return None # notification — no response
353
+
354
+ if method == "ping":
355
+ return _jsonrpc_result(req_id, {})
356
+
357
+ # --- Tool discovery -----------------------------------------------------
358
+ if method == "tools/list":
359
+ return _jsonrpc_result(req_id, {"tools": _tool_schemas()})
360
+
361
+ # --- Tool execution -----------------------------------------------------
362
+ if method == "tools/call":
363
+ tool_name = params.get("name", "")
364
+ arguments = params.get("arguments", {})
365
+ try:
366
+ result = _dispatch_tool(tool_name, arguments)
367
+ text = _json.dumps(result, default=str)
368
+ return _jsonrpc_result(req_id, {
369
+ "content": [{"type": "text", "text": text}],
370
+ })
371
+ except Exception as exc:
372
+ return _jsonrpc_result(req_id, {
373
+ "content": [{"type": "text", "text": str(exc)}],
374
+ "isError": True,
375
+ })
376
+
377
+ # Notifications we don't handle — silently ignore
378
+ if req_id is None:
379
+ return None
380
+
381
+ return _jsonrpc_error(req_id, -32601, f"Method not found: {method}")
382
+
383
+
384
+ def _get_version() -> str:
385
+ try:
386
+ from avrotize._version import version
387
+ return version
388
+ except Exception:
389
+ return "dev"
390
+
391
+
392
+ def _run_stdio_loop() -> None:
393
+ """Read JSON-RPC messages from stdin, write responses to stdout."""
394
+ reader = sys.stdin
395
+ writer = sys.stdout
396
+ for line in reader:
397
+ line = line.strip()
398
+ if not line:
399
+ continue
400
+ try:
401
+ msg = _json.loads(line)
402
+ except _json.JSONDecodeError:
403
+ resp = _jsonrpc_error(None, -32700, "Parse error")
404
+ writer.write(_json.dumps(resp) + "\n")
405
+ writer.flush()
406
+ continue
407
+
408
+ resp = _handle_message(msg)
409
+ if resp is not None:
410
+ writer.write(_json.dumps(resp) + "\n")
411
+ writer.flush()
412
+
413
+
414
+ def run_mcp_server(transport: str = "stdio"):
415
+ """Run avrotize as a local MCP server.
416
+
417
+ Uses a lightweight built-in JSON-RPC stdio implementation by default.
418
+ """
419
+ if transport != "stdio":
420
+ raise ValueError("Only 'stdio' transport is currently supported.")
421
+ _run_stdio_loop()
422
+
423
+
424
+ def run_mcp_server_fastmcp(transport: str = "stdio"):
425
+ """Run avrotize as a local MCP server using the ``mcp`` library.
426
+
427
+ This is slower to start (~2-3 s) because the ``mcp`` library eagerly
428
+ imports uvicorn, starlette, and httpx. Kept as a compatibility fallback.
429
+ """
430
+ try:
431
+ from mcp.server.fastmcp import FastMCP
432
+ except ImportError as exc:
433
+ raise RuntimeError(
434
+ "MCP support is not installed. Install with: pip install mcp"
435
+ ) from exc
436
+
437
+ if transport != "stdio":
438
+ raise ValueError("Only 'stdio' transport is currently supported.")
439
+
440
+ mcp = FastMCP("avrotize")
441
+
442
+ @mcp.tool()
443
+ def describe_capabilities() -> Dict[str, Any]:
444
+ """Describe when this server should be used and how to invoke it."""
445
+ return _describe_capabilities()
446
+
447
+ @mcp.tool()
448
+ def list_conversions() -> List[Dict[str, Any]]:
449
+ """List available Avrotize conversion commands."""
450
+ return _list_commands()
451
+
452
+ @mcp.tool()
453
+ def get_conversion(command: str) -> Dict[str, Any]:
454
+ """Get metadata for a specific conversion command."""
455
+ cmd = _find_command(command)
456
+ if not cmd:
457
+ raise ValueError(f"Unknown command '{command}'.")
458
+ return {
459
+ "command": cmd.get("command"),
460
+ "description": cmd.get("description"),
461
+ "group": cmd.get("group"),
462
+ "args": cmd.get("args", []),
463
+ }
464
+
465
+ @mcp.tool()
466
+ def run_conversion(
467
+ command: str,
468
+ input_path: str = "",
469
+ input_content: str = "",
470
+ output_path: str = "",
471
+ options: Dict[str, Any] | None = None,
472
+ ) -> Dict[str, Any]:
473
+ """Run a conversion command and return conversion output information."""
474
+ return _execute_conversion(
475
+ command_name=command,
476
+ input_path=input_path or None,
477
+ input_content=input_content or None,
478
+ output_path=output_path or None,
479
+ options=options or {},
480
+ )
481
+
482
+ try:
483
+ mcp.run(transport="stdio")
484
+ except TypeError:
485
+ mcp.run()
486
+
487
+
488
+ # ---------------------------------------------------------------------------
489
+ # Direct entry point — bypasses the avrotize CLI argparse overhead.
490
+ # ---------------------------------------------------------------------------
491
+
492
+ def main() -> None:
493
+ """Entry point for the ``avrotize-mcp`` console script."""
494
+ run_mcp_server("stdio")
495
+
496
+
497
+ if __name__ == "__main__":
498
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structurize
3
- Version: 3.4.2
3
+ Version: 3.5.0
4
4
  Summary: Tools to convert from and to JSON Structure from various other schema languages.
5
5
  Author-email: Clemens Vasters <clemensv@microsoft.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -120,6 +120,7 @@ pyproject.toml
120
120
  ../avrotize/avrotots/package.json.jinja
121
121
  ../avrotize/avrotots/tsconfig.json.jinja
122
122
  ../avrotize/dependencies/cpp/vcpkg/vcpkg.json
123
+ ../avrotize/dependencies/cs/net100/dependencies.csproj
123
124
  ../avrotize/dependencies/cs/net90/dependencies.csproj
124
125
  ../avrotize/dependencies/go/go121/go.mod
125
126
  ../avrotize/dependencies/java/jdk21/pom.xml
@@ -1,253 +0,0 @@
1
- """MCP server integration for Avrotize conversions."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import os
7
- import tempfile
8
- from typing import Any, Dict, List
9
-
10
- from avrotize.avrotize import dynamic_import, load_commands
11
-
12
-
13
- def _command_dest(arg: Dict[str, Any]) -> str:
14
- name = arg["name"]
15
- if name.startswith("-"):
16
- return arg.get("dest", name.lstrip("-").replace("-", "_").replace(".", "_"))
17
- return name
18
-
19
-
20
- def _coerce_value(value: Any, arg_type: str) -> Any:
21
- if value is None:
22
- return None
23
- if arg_type == "bool":
24
- if isinstance(value, bool):
25
- return value
26
- if isinstance(value, (int, float)):
27
- return bool(value)
28
- sval = str(value).strip().lower()
29
- return sval in {"1", "true", "yes", "y", "on"}
30
- if arg_type == "int":
31
- return int(value)
32
- if arg_type == "float":
33
- return float(value)
34
- return str(value) if arg_type == "str" else value
35
-
36
-
37
- def _build_namespace(command: Dict[str, Any], options: Dict[str, Any]) -> argparse.Namespace:
38
- namespace = argparse.Namespace()
39
- normalized_options = {
40
- key.lstrip("-").replace("-", "_"): value
41
- for key, value in (options or {}).items()
42
- }
43
-
44
- for arg in command.get("args", []):
45
- dest = _command_dest(arg)
46
- default_value = arg.get("default", False if arg.get("type") == "bool" else None)
47
- setattr(namespace, dest, default_value)
48
- if dest in normalized_options:
49
- setattr(namespace, dest, _coerce_value(normalized_options[dest], arg["type"]))
50
-
51
- return namespace
52
-
53
-
54
- def _resolve_input_path(command_args: argparse.Namespace, explicit_input_path: str | None) -> str | None:
55
- if explicit_input_path:
56
- return explicit_input_path
57
- return (
58
- getattr(command_args, "input", None)
59
- or getattr(command_args, "avsc", None)
60
- or getattr(command_args, "proto", None)
61
- or getattr(command_args, "jsons", None)
62
- or getattr(command_args, "xsd", None)
63
- or getattr(command_args, "kusto_uri", None)
64
- or getattr(command_args, "parquet", None)
65
- or getattr(command_args, "asn", None)
66
- or getattr(command_args, "kstruct", None)
67
- )
68
-
69
-
70
- def _find_command(command_name: str) -> Dict[str, Any] | None:
71
- return next((cmd for cmd in load_commands() if cmd.get("command") == command_name), None)
72
-
73
-
74
- def _list_commands() -> List[Dict[str, Any]]:
75
- commands = load_commands()
76
- result: List[Dict[str, Any]] = []
77
- for command in commands:
78
- if command.get("command") == "mcp":
79
- continue
80
- result.append(
81
- {
82
- "command": command.get("command"),
83
- "description": command.get("description"),
84
- "group": command.get("group"),
85
- "args": [arg.get("name") for arg in command.get("args", [])],
86
- }
87
- )
88
- return result
89
-
90
-
91
- def _describe_capabilities() -> Dict[str, Any]:
92
- commands = load_commands()
93
- command_names = [cmd.get("command") for cmd in commands if cmd.get("command") != "mcp"]
94
- return {
95
- "server": "avrotize",
96
- "purpose": "Schema conversion and schema-driven code generation",
97
- "use_when": [
98
- "Converting between schema formats (Avro, JSON Schema, Proto, XSD, Parquet, etc.)",
99
- "Generating code from Avro/JSON Structure schemas (C#, Java, Python, TypeScript, JavaScript, Rust, Go, C++)",
100
- "Inferring schemas from sample JSON/XML/CSV/parquet inputs",
101
- ],
102
- "do_not_use_when": [
103
- "Task is unrelated to schema conversion or code generation",
104
- "Task requires arbitrary code execution outside avrotize command set",
105
- ],
106
- "recommended_flow": [
107
- "Call describe_capabilities for high-level routing",
108
- "Call list_conversions to discover available commands",
109
- "Call get_conversion(command) to inspect required args/options",
110
- "Call run_conversion(...) to execute",
111
- ],
112
- "tools": {
113
- "describe_capabilities": "High-level guidance for when and how to use this server",
114
- "list_conversions": "List available conversion/code generation commands",
115
- "get_conversion": "Inspect metadata and args for a specific command",
116
- "run_conversion": "Execute a specific conversion command",
117
- },
118
- "command_count": len(command_names),
119
- "commands": command_names,
120
- }
121
-
122
-
123
- def _execute_conversion(
124
- command_name: str,
125
- input_path: str | None = None,
126
- input_content: str | None = None,
127
- output_path: str | None = None,
128
- options: Dict[str, Any] | None = None,
129
- ) -> Dict[str, Any]:
130
- command = _find_command(command_name)
131
- if not command:
132
- raise ValueError(f"Unknown command '{command_name}'.")
133
-
134
- command_args = _build_namespace(command, options or {})
135
- temp_input_path = None
136
- temp_output_path = None
137
-
138
- try:
139
- resolved_input = _resolve_input_path(command_args, input_path)
140
- skip_input_file_handling = command.get("skip_input_file_handling", False)
141
- if not skip_input_file_handling and not resolved_input:
142
- if input_content is None:
143
- raise ValueError("This command requires input_path or input_content.")
144
- temp_input = tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8")
145
- temp_input.write(input_content)
146
- temp_input.flush()
147
- temp_input.close()
148
- temp_input_path = temp_input.name
149
- resolved_input = temp_input_path
150
-
151
- if not output_path and any(v == "output_file_path" for v in command.get("function", {}).get("args", {}).values()):
152
- temp_output = tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8")
153
- temp_output_path = temp_output.name
154
- temp_output.close()
155
- output_path = temp_output_path
156
-
157
- module_name, func_name = command["function"]["name"].rsplit(".", 1)
158
- func = dynamic_import(module_name, func_name)
159
-
160
- func_args: Dict[str, Any] = {}
161
- for arg_name, arg_value in command["function"]["args"].items():
162
- if arg_value == "input_file_path":
163
- func_args[arg_name] = resolved_input
164
- elif arg_value == "output_file_path":
165
- func_args[arg_name] = output_path
166
- elif isinstance(arg_value, str) and arg_value.startswith("args."):
167
- attr_name = arg_value[5:]
168
- if hasattr(command_args, attr_name):
169
- func_args[arg_name] = getattr(command_args, attr_name)
170
- else:
171
- func_args[arg_name] = arg_value
172
-
173
- func(**func_args)
174
-
175
- output_content = None
176
- if temp_output_path and os.path.exists(temp_output_path):
177
- with open(temp_output_path, "r", encoding="utf-8") as out_file:
178
- output_content = out_file.read()
179
-
180
- return {
181
- "success": True,
182
- "command": command_name,
183
- "output_path": output_path,
184
- "output_content": output_content,
185
- }
186
- finally:
187
- if temp_input_path and os.path.exists(temp_input_path):
188
- os.remove(temp_input_path)
189
- if temp_output_path and os.path.exists(temp_output_path):
190
- os.remove(temp_output_path)
191
-
192
-
193
- def run_mcp_server(transport: str = "stdio"):
194
- """Run avrotize as a local MCP server.
195
-
196
- Exposes conversion tools to MCP clients.
197
- """
198
- try:
199
- from mcp.server.fastmcp import FastMCP
200
- except ImportError as exc:
201
- raise RuntimeError(
202
- "MCP support is not installed. Install with: pip install mcp"
203
- ) from exc
204
-
205
- if transport != "stdio":
206
- raise ValueError("Only 'stdio' transport is currently supported.")
207
-
208
- mcp = FastMCP("avrotize")
209
-
210
- @mcp.tool()
211
- def describe_capabilities() -> Dict[str, Any]:
212
- """Describe when this server should be used and how to invoke it."""
213
- return _describe_capabilities()
214
-
215
- @mcp.tool()
216
- def list_conversions() -> List[Dict[str, Any]]:
217
- """List available Avrotize conversion commands."""
218
- return _list_commands()
219
-
220
- @mcp.tool()
221
- def get_conversion(command: str) -> Dict[str, Any]:
222
- """Get metadata for a specific conversion command."""
223
- cmd = _find_command(command)
224
- if not cmd:
225
- raise ValueError(f"Unknown command '{command}'.")
226
- return {
227
- "command": cmd.get("command"),
228
- "description": cmd.get("description"),
229
- "group": cmd.get("group"),
230
- "args": cmd.get("args", []),
231
- }
232
-
233
- @mcp.tool()
234
- def run_conversion(
235
- command: str,
236
- input_path: str = "",
237
- input_content: str = "",
238
- output_path: str = "",
239
- options: Dict[str, Any] | None = None,
240
- ) -> Dict[str, Any]:
241
- """Run a conversion command and return conversion output information."""
242
- return _execute_conversion(
243
- command_name=command,
244
- input_path=input_path or None,
245
- input_content=input_content or None,
246
- output_path=output_path or None,
247
- options=options or {},
248
- )
249
-
250
- try:
251
- mcp.run(transport="stdio")
252
- except TypeError:
253
- mcp.run()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes