@dpantani/tdmcp 0.1.0 → 0.2.0

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.
@@ -1 +1 @@
1
- {"source":"bottobot","bottobotVersion":"2.8.0","importedAt":"2026-05-25T21:00:49.222Z","counts":{"operators":629,"pythonClasses":68,"tutorials":14,"patterns":32,"glsl":7}}
1
+ {"source":"bottobot","bottobotVersion":"2.8.0","importedAt":"2026-05-26T08:21:03.449Z","counts":{"operators":629,"pythonClasses":68,"tutorials":14,"patterns":32,"glsl":7}}
@@ -21,14 +21,14 @@
21
21
  {
22
22
  "name": "transform1",
23
23
  "type": "transformTOP",
24
- "parameters": { "scalex": 1.03, "scaley": 1.03, "rotate": 1.5 },
24
+ "parameters": { "sx": 1.03, "sy": 1.03, "rotate": 1.5 },
25
25
  "comment": "Zoom + rotate each frame to create the tunnel motion"
26
26
  },
27
27
  { "name": "blur1", "type": "blurTOP", "parameters": { "size": 2 } },
28
28
  {
29
29
  "name": "level1",
30
30
  "type": "levelTOP",
31
- "parameters": { "gain": 0.96 },
31
+ "parameters": { "brightness1": 0.96 },
32
32
  "comment": "Decay / feedback gain"
33
33
  },
34
34
  { "name": "out1", "type": "nullTOP", "comment": "Output of the system" }
@@ -53,7 +53,7 @@
53
53
  {
54
54
  "name": "feedback_gain",
55
55
  "node": "level1",
56
- "param": "gain",
56
+ "param": "brightness1",
57
57
  "value": 0.96,
58
58
  "min": 0,
59
59
  "max": 1,
@@ -62,7 +62,7 @@
62
62
  {
63
63
  "name": "zoom",
64
64
  "node": "transform1",
65
- "param": "scalex",
65
+ "param": "sx",
66
66
  "value": 1.03,
67
67
  "min": 1,
68
68
  "max": 1.2,
@@ -70,7 +70,108 @@
70
70
  "min": 0,
71
71
  "max": 2,
72
72
  "label": "Terrain Height"
73
+ },
74
+ {
75
+ "name": "sky_r",
76
+ "node": "render1",
77
+ "param": "bgcolorr",
78
+ "value": 0.06,
79
+ "description": "Background sky red — deep indigo atmosphere instead of empty white void."
80
+ },
81
+ {
82
+ "name": "sky_g",
83
+ "node": "render1",
84
+ "param": "bgcolorg",
85
+ "value": 0.07,
86
+ "description": "Background sky green."
87
+ },
88
+ {
89
+ "name": "sky_b",
90
+ "node": "render1",
91
+ "param": "bgcolorb",
92
+ "value": 0.14,
93
+ "description": "Background sky blue."
94
+ },
95
+ {
96
+ "name": "sky_a",
97
+ "node": "render1",
98
+ "param": "bgcolora",
99
+ "value": 1,
100
+ "description": "Background alpha = 1 so the sky is opaque (not transparent/white)."
101
+ },
102
+ {
103
+ "name": "terrain_diffuse_r",
104
+ "node": "phong1",
105
+ "param": "diffr",
106
+ "value": 0.3,
107
+ "description": "Terrain diffuse red — mossy green so the landscape is coloured, not default grey."
108
+ },
109
+ {
110
+ "name": "terrain_diffuse_g",
111
+ "node": "phong1",
112
+ "param": "diffg",
113
+ "value": 0.58,
114
+ "description": "Terrain diffuse green."
115
+ },
116
+ {
117
+ "name": "terrain_diffuse_b",
118
+ "node": "phong1",
119
+ "param": "diffb",
120
+ "value": 0.42,
121
+ "description": "Terrain diffuse blue."
122
+ },
123
+ {
124
+ "name": "terrain_ambient_r",
125
+ "node": "phong1",
126
+ "param": "ambr",
127
+ "value": 0.07,
128
+ "description": "Ambient red — cool indigo fill matching the sky so shadow sides read as atmospheric depth, not black."
129
+ },
130
+ {
131
+ "name": "terrain_ambient_g",
132
+ "node": "phong1",
133
+ "param": "ambg",
134
+ "value": 0.11,
135
+ "description": "Ambient green."
136
+ },
137
+ {
138
+ "name": "terrain_ambient_b",
139
+ "node": "phong1",
140
+ "param": "ambb",
141
+ "value": 0.2,
142
+ "description": "Ambient blue (indigo fill)."
143
+ },
144
+ {
145
+ "name": "terrain_specular_r",
146
+ "node": "phong1",
147
+ "param": "specr",
148
+ "value": 0.7,
149
+ "description": "Specular red — warm sun glint catching the hill tops."
150
+ },
151
+ {
152
+ "name": "terrain_specular_g",
153
+ "node": "phong1",
154
+ "param": "specg",
155
+ "value": 0.62,
156
+ "description": "Specular green."
157
+ },
158
+ {
159
+ "name": "terrain_specular_b",
160
+ "node": "phong1",
161
+ "param": "specb",
162
+ "value": 0.4,
163
+ "description": "Specular blue (warm)."
164
+ },
165
+ {
166
+ "name": "terrain_shininess",
167
+ "node": "phong1",
168
+ "param": "shininess",
169
+ "value": 30,
170
+ "min": 0,
171
+ "max": 100,
172
+ "label": "Highlight Tightness",
173
+ "description": "Phong shininess — tighter sun highlight on the ridges."
73
174
  }
74
175
  ],
75
- "preview_description": "A softly lit rolling 3D terrain of noise-driven hills viewed from a slightly elevated angle."
176
+ "preview_description": "A green-teal rolling 3D terrain, sun-lit with warm specular highlights on the ridges and cool indigo ambient in the valleys, set against a deep indigo atmospheric sky."
76
177
  }
@@ -0,0 +1,92 @@
1
+ {
2
+ "id": "performable_feedback_tunnel",
3
+ "name": "Performable Feedback Tunnel",
4
+ "description": "A feedback tunnel that ships with live controls: knobs for feedback decay, zoom, spin and blur are exposed on the container so you can perform it, animate the knobs with an LFO, or snapshot looks as presets.",
5
+ "tags": [
6
+ "feedback",
7
+ "generative",
8
+ "tunnel",
9
+ "abstract",
10
+ "noise",
11
+ "performable",
12
+ "controls",
13
+ "vj"
14
+ ],
15
+ "difficulty": "intermediate",
16
+ "td_version_min": "2022",
17
+ "nodes": [
18
+ {
19
+ "name": "noise1",
20
+ "type": "noiseTOP",
21
+ "parameters": { "period": 3, "mono": 1 },
22
+ "comment": "Seed texture that feeds the loop (mono=1 keeps it grayscale; the param is 'mono', not 'monochrome')"
23
+ },
24
+ { "name": "feedback1", "type": "feedbackTOP", "comment": "Holds the previous frame" },
25
+ {
26
+ "name": "comp1",
27
+ "type": "compositeTOP",
28
+ "parameters": { "operand": "maximum" },
29
+ "comment": "Combines seed + fed-back frame. 'maximum' stays bounded under feedback gain (the default multiply collapses to black)."
30
+ },
31
+ {
32
+ "name": "transform1",
33
+ "type": "transformTOP",
34
+ "parameters": { "sx": 1.03, "sy": 1.03, "rotate": 1.5 },
35
+ "comment": "Zoom + rotate each frame to create the tunnel motion (sx/sy are the scale params, not scalex/scaley)"
36
+ },
37
+ { "name": "blur1", "type": "blurTOP", "parameters": { "size": 2 } },
38
+ {
39
+ "name": "level1",
40
+ "type": "levelTOP",
41
+ "parameters": { "brightness1": 0.96 },
42
+ "comment": "Decay / feedback gain. levelTOP multiplies RGB via brightness1 (it has no 'gain' parameter)."
43
+ },
44
+ { "name": "out1", "type": "nullTOP", "comment": "Output of the system" }
45
+ ],
46
+ "connections": [
47
+ { "from": "noise1", "to": "feedback1" },
48
+ { "from": "noise1", "to": "comp1", "to_input": 0 },
49
+ { "from": "feedback1", "to": "comp1", "to_input": 1 },
50
+ { "from": "comp1", "to": "transform1" },
51
+ { "from": "transform1", "to": "blur1" },
52
+ { "from": "blur1", "to": "level1" },
53
+ { "from": "level1", "to": "out1" }
54
+ ],
55
+ "parameters": [
56
+ {
57
+ "name": "feedback_target",
58
+ "node": "feedback1",
59
+ "param": "top",
60
+ "value": "level1",
61
+ "description": "feedbackTOP samples the loop output (level1). Value resolves to the created node path."
62
+ }
63
+ ],
64
+ "controls": [
65
+ {
66
+ "name": "Feedback",
67
+ "type": "float",
68
+ "min": 0,
69
+ "max": 1,
70
+ "default": 0.96,
71
+ "bind_to": ["level1.brightness1"]
72
+ },
73
+ {
74
+ "name": "Zoom",
75
+ "type": "float",
76
+ "min": 1,
77
+ "max": 1.2,
78
+ "default": 1.03,
79
+ "bind_to": ["transform1.sx", "transform1.sy"]
80
+ },
81
+ {
82
+ "name": "Spin",
83
+ "type": "float",
84
+ "min": -5,
85
+ "max": 5,
86
+ "default": 1.5,
87
+ "bind_to": ["transform1.rotate"]
88
+ },
89
+ { "name": "Blur", "type": "float", "min": 0, "max": 8, "default": 2, "bind_to": ["blur1.size"] }
90
+ ],
91
+ "preview_description": "An endless rotating tunnel of evolving colored noise that folds into its own center, with live knobs for feedback, zoom, spin and blur."
92
+ }
@@ -10,7 +10,11 @@
10
10
  { "name": "edge", "type": "edgeTOP" },
11
11
  { "name": "rgbsplit", "type": "glslTOP", "comment": "Chromatic RGB offset" },
12
12
  { "name": "feedback1", "type": "feedbackTOP" },
13
- { "name": "comp1", "type": "compositeTOP" },
13
+ {
14
+ "name": "comp1",
15
+ "type": "compositeTOP",
16
+ "parameters": { "outputresolution": "custom", "resolutionw": 1280, "resolutionh": 720 }
17
+ },
14
18
  { "name": "glitch", "type": "glslTOP", "comment": "Per-band horizontal displacement" },
15
19
  { "name": "out1", "type": "nullTOP" }
16
20
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpantani/tdmcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AI-native visual creation for TouchDesigner — a production-grade MCP server.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,14 +21,14 @@
21
21
  {
22
22
  "name": "transform1",
23
23
  "type": "transformTOP",
24
- "parameters": { "scalex": 1.03, "scaley": 1.03, "rotate": 1.5 },
24
+ "parameters": { "sx": 1.03, "sy": 1.03, "rotate": 1.5 },
25
25
  "comment": "Zoom + rotate each frame to create the tunnel motion"
26
26
  },
27
27
  { "name": "blur1", "type": "blurTOP", "parameters": { "size": 2 } },
28
28
  {
29
29
  "name": "level1",
30
30
  "type": "levelTOP",
31
- "parameters": { "gain": 0.96 },
31
+ "parameters": { "brightness1": 0.96 },
32
32
  "comment": "Decay / feedback gain"
33
33
  },
34
34
  { "name": "out1", "type": "nullTOP", "comment": "Output of the system" }
@@ -53,7 +53,7 @@
53
53
  {
54
54
  "name": "feedback_gain",
55
55
  "node": "level1",
56
- "param": "gain",
56
+ "param": "brightness1",
57
57
  "value": 0.96,
58
58
  "min": 0,
59
59
  "max": 1,
@@ -62,7 +62,7 @@
62
62
  {
63
63
  "name": "zoom",
64
64
  "node": "transform1",
65
- "param": "scalex",
65
+ "param": "sx",
66
66
  "value": 1.03,
67
67
  "min": 1,
68
68
  "max": 1.2,
@@ -70,7 +70,108 @@
70
70
  "min": 0,
71
71
  "max": 2,
72
72
  "label": "Terrain Height"
73
+ },
74
+ {
75
+ "name": "sky_r",
76
+ "node": "render1",
77
+ "param": "bgcolorr",
78
+ "value": 0.06,
79
+ "description": "Background sky red — deep indigo atmosphere instead of empty white void."
80
+ },
81
+ {
82
+ "name": "sky_g",
83
+ "node": "render1",
84
+ "param": "bgcolorg",
85
+ "value": 0.07,
86
+ "description": "Background sky green."
87
+ },
88
+ {
89
+ "name": "sky_b",
90
+ "node": "render1",
91
+ "param": "bgcolorb",
92
+ "value": 0.14,
93
+ "description": "Background sky blue."
94
+ },
95
+ {
96
+ "name": "sky_a",
97
+ "node": "render1",
98
+ "param": "bgcolora",
99
+ "value": 1,
100
+ "description": "Background alpha = 1 so the sky is opaque (not transparent/white)."
101
+ },
102
+ {
103
+ "name": "terrain_diffuse_r",
104
+ "node": "phong1",
105
+ "param": "diffr",
106
+ "value": 0.3,
107
+ "description": "Terrain diffuse red — mossy green so the landscape is coloured, not default grey."
108
+ },
109
+ {
110
+ "name": "terrain_diffuse_g",
111
+ "node": "phong1",
112
+ "param": "diffg",
113
+ "value": 0.58,
114
+ "description": "Terrain diffuse green."
115
+ },
116
+ {
117
+ "name": "terrain_diffuse_b",
118
+ "node": "phong1",
119
+ "param": "diffb",
120
+ "value": 0.42,
121
+ "description": "Terrain diffuse blue."
122
+ },
123
+ {
124
+ "name": "terrain_ambient_r",
125
+ "node": "phong1",
126
+ "param": "ambr",
127
+ "value": 0.07,
128
+ "description": "Ambient red — cool indigo fill matching the sky so shadow sides read as atmospheric depth, not black."
129
+ },
130
+ {
131
+ "name": "terrain_ambient_g",
132
+ "node": "phong1",
133
+ "param": "ambg",
134
+ "value": 0.11,
135
+ "description": "Ambient green."
136
+ },
137
+ {
138
+ "name": "terrain_ambient_b",
139
+ "node": "phong1",
140
+ "param": "ambb",
141
+ "value": 0.2,
142
+ "description": "Ambient blue (indigo fill)."
143
+ },
144
+ {
145
+ "name": "terrain_specular_r",
146
+ "node": "phong1",
147
+ "param": "specr",
148
+ "value": 0.7,
149
+ "description": "Specular red — warm sun glint catching the hill tops."
150
+ },
151
+ {
152
+ "name": "terrain_specular_g",
153
+ "node": "phong1",
154
+ "param": "specg",
155
+ "value": 0.62,
156
+ "description": "Specular green."
157
+ },
158
+ {
159
+ "name": "terrain_specular_b",
160
+ "node": "phong1",
161
+ "param": "specb",
162
+ "value": 0.4,
163
+ "description": "Specular blue (warm)."
164
+ },
165
+ {
166
+ "name": "terrain_shininess",
167
+ "node": "phong1",
168
+ "param": "shininess",
169
+ "value": 30,
170
+ "min": 0,
171
+ "max": 100,
172
+ "label": "Highlight Tightness",
173
+ "description": "Phong shininess — tighter sun highlight on the ridges."
73
174
  }
74
175
  ],
75
- "preview_description": "A softly lit rolling 3D terrain of noise-driven hills viewed from a slightly elevated angle."
176
+ "preview_description": "A green-teal rolling 3D terrain, sun-lit with warm specular highlights on the ridges and cool indigo ambient in the valleys, set against a deep indigo atmospheric sky."
76
177
  }
@@ -0,0 +1,92 @@
1
+ {
2
+ "id": "performable_feedback_tunnel",
3
+ "name": "Performable Feedback Tunnel",
4
+ "description": "A feedback tunnel that ships with live controls: knobs for feedback decay, zoom, spin and blur are exposed on the container so you can perform it, animate the knobs with an LFO, or snapshot looks as presets.",
5
+ "tags": [
6
+ "feedback",
7
+ "generative",
8
+ "tunnel",
9
+ "abstract",
10
+ "noise",
11
+ "performable",
12
+ "controls",
13
+ "vj"
14
+ ],
15
+ "difficulty": "intermediate",
16
+ "td_version_min": "2022",
17
+ "nodes": [
18
+ {
19
+ "name": "noise1",
20
+ "type": "noiseTOP",
21
+ "parameters": { "period": 3, "mono": 1 },
22
+ "comment": "Seed texture that feeds the loop (mono=1 keeps it grayscale; the param is 'mono', not 'monochrome')"
23
+ },
24
+ { "name": "feedback1", "type": "feedbackTOP", "comment": "Holds the previous frame" },
25
+ {
26
+ "name": "comp1",
27
+ "type": "compositeTOP",
28
+ "parameters": { "operand": "maximum" },
29
+ "comment": "Combines seed + fed-back frame. 'maximum' stays bounded under feedback gain (the default multiply collapses to black)."
30
+ },
31
+ {
32
+ "name": "transform1",
33
+ "type": "transformTOP",
34
+ "parameters": { "sx": 1.03, "sy": 1.03, "rotate": 1.5 },
35
+ "comment": "Zoom + rotate each frame to create the tunnel motion (sx/sy are the scale params, not scalex/scaley)"
36
+ },
37
+ { "name": "blur1", "type": "blurTOP", "parameters": { "size": 2 } },
38
+ {
39
+ "name": "level1",
40
+ "type": "levelTOP",
41
+ "parameters": { "brightness1": 0.96 },
42
+ "comment": "Decay / feedback gain. levelTOP multiplies RGB via brightness1 (it has no 'gain' parameter)."
43
+ },
44
+ { "name": "out1", "type": "nullTOP", "comment": "Output of the system" }
45
+ ],
46
+ "connections": [
47
+ { "from": "noise1", "to": "feedback1" },
48
+ { "from": "noise1", "to": "comp1", "to_input": 0 },
49
+ { "from": "feedback1", "to": "comp1", "to_input": 1 },
50
+ { "from": "comp1", "to": "transform1" },
51
+ { "from": "transform1", "to": "blur1" },
52
+ { "from": "blur1", "to": "level1" },
53
+ { "from": "level1", "to": "out1" }
54
+ ],
55
+ "parameters": [
56
+ {
57
+ "name": "feedback_target",
58
+ "node": "feedback1",
59
+ "param": "top",
60
+ "value": "level1",
61
+ "description": "feedbackTOP samples the loop output (level1). Value resolves to the created node path."
62
+ }
63
+ ],
64
+ "controls": [
65
+ {
66
+ "name": "Feedback",
67
+ "type": "float",
68
+ "min": 0,
69
+ "max": 1,
70
+ "default": 0.96,
71
+ "bind_to": ["level1.brightness1"]
72
+ },
73
+ {
74
+ "name": "Zoom",
75
+ "type": "float",
76
+ "min": 1,
77
+ "max": 1.2,
78
+ "default": 1.03,
79
+ "bind_to": ["transform1.sx", "transform1.sy"]
80
+ },
81
+ {
82
+ "name": "Spin",
83
+ "type": "float",
84
+ "min": -5,
85
+ "max": 5,
86
+ "default": 1.5,
87
+ "bind_to": ["transform1.rotate"]
88
+ },
89
+ { "name": "Blur", "type": "float", "min": 0, "max": 8, "default": 2, "bind_to": ["blur1.size"] }
90
+ ],
91
+ "preview_description": "An endless rotating tunnel of evolving colored noise that folds into its own center, with live knobs for feedback, zoom, spin and blur."
92
+ }
@@ -10,7 +10,11 @@
10
10
  { "name": "edge", "type": "edgeTOP" },
11
11
  { "name": "rgbsplit", "type": "glslTOP", "comment": "Chromatic RGB offset" },
12
12
  { "name": "feedback1", "type": "feedbackTOP" },
13
- { "name": "comp1", "type": "compositeTOP" },
13
+ {
14
+ "name": "comp1",
15
+ "type": "compositeTOP",
16
+ "parameters": { "outputresolution": "custom", "resolutionw": 1280, "resolutionh": 720 }
17
+ },
14
18
  { "name": "glitch", "type": "glslTOP", "comment": "Per-band horizontal displacement" },
15
19
  { "name": "out1", "type": "nullTOP" }
16
20
  ],
@@ -74,6 +74,31 @@ def _check_auth(request):
74
74
  raise PermissionError("Unauthorized: missing or invalid bearer token.")
75
75
 
76
76
 
77
+ _LOOPBACK_HOSTS = ("127.0.0.1", "localhost", "::1")
78
+
79
+
80
+ def _check_origin(request):
81
+ """Reject browser-originated cross-origin requests (CSRF / DNS-rebinding).
82
+
83
+ The Node MCP server — the only legitimate caller — never sends an `Origin`
84
+ header. Browsers always attach one on cross-site requests, so a request
85
+ bearing a non-loopback `Origin` can only be a web page trying to drive the
86
+ bridge (e.g. a malicious site POSTing to http://127.0.0.1:9980/api/exec).
87
+ Under the default zero-auth + exec-on config that would be drive-by remote
88
+ code execution, so refuse it. Loopback origins stay allowed (a locally
89
+ served tool page still works) and same-origin / no-Origin callers are
90
+ unaffected. Holds even against a direct caller and independent of the
91
+ optional bearer token, mirroring the Node HTTP transport's DNS-rebinding
92
+ guard on its own port.
93
+ """
94
+ origin = _find_header(request, "origin")
95
+ if not origin:
96
+ return
97
+ host = urlparse(origin).hostname
98
+ if host not in _LOOPBACK_HOSTS:
99
+ raise PermissionError("Forbidden: cross-origin request rejected (origin %r)." % origin)
100
+
101
+
77
102
  def _qs(query, key, default=None):
78
103
  values = query.get(key)
79
104
  return values[0] if values else default
@@ -160,7 +185,9 @@ def _route(method, path, query, body):
160
185
  if kind == "topology":
161
186
  return analysis_service.topology(node_path, recursive=_qs(query, "recursive") == "true")
162
187
  if kind == "performance":
163
- return analysis_service.performance(node_path)
188
+ return analysis_service.performance(
189
+ node_path, recursive=_qs(query, "recursive") == "true"
190
+ )
164
191
 
165
192
  raise ValueError("Unsupported %s %s" % (method, path))
166
193
 
@@ -226,6 +253,7 @@ def _emit_event(webserver, method, path, data):
226
253
 
227
254
  def handle(request, response, webserver=None):
228
255
  try:
256
+ _check_origin(request)
229
257
  _check_auth(request)
230
258
  method = (request.get("method") or "GET").upper()
231
259
  parsed = urlparse(request.get("uri", "/"))
@@ -44,13 +44,18 @@ def topology(path, recursive=False):
44
44
  return {"nodes": nodes, "connections": connections}
45
45
 
46
46
 
47
- def performance(path):
47
+ def performance(path, recursive=False):
48
48
  root = op(path) # noqa: F821
49
49
  if root is None:
50
50
  return {"nodes": [], "total_cook_time_ms": 0.0}
51
51
  nodes = []
52
52
  total = 0.0
53
- children = root.findChildren(depth=1) if hasattr(root, "findChildren") else []
53
+ if not hasattr(root, "findChildren"):
54
+ children = []
55
+ elif recursive:
56
+ children = root.findChildren() # all descendants (cook time of nested nodes too)
57
+ else:
58
+ children = root.findChildren(depth=1) # direct children only
54
59
  for child in children:
55
60
  cook_time = float(getattr(child, "cookTime", 0.0) or 0.0)
56
61
  total += cook_time
@@ -71,9 +71,15 @@ def create_node(parent_path, type_name, name=None, parameters=None):
71
71
  raise LookupError("Parent not found: %s" % parent_path)
72
72
  cls = _resolve_type(type_name)
73
73
  node = parent.create(cls, name) if name else parent.create(cls)
74
+ ref = node_ref(node)
74
75
  if parameters:
75
- apply_parameters(node, parameters)
76
- return node_ref(node)
76
+ # The node is created regardless; surface any params that did not apply
77
+ # (unknown name or bad value) as a non-fatal warning rather than dropping
78
+ # them silently. The caller (create_td_node) relays these to the user.
79
+ _applied, failed = apply_parameters(node, parameters)
80
+ if failed:
81
+ ref["parameter_warnings"] = sorted(failed)
82
+ return ref
77
83
 
78
84
 
79
85
  def delete_node(path):
@@ -120,7 +126,23 @@ def update_parameters(path, parameters):
120
126
  node = op(path) # noqa: F821
121
127
  if node is None:
122
128
  raise LookupError("Node not found: %s" % path)
123
- apply_parameters(node, parameters)
129
+ params = parameters or {}
130
+ # Reject unknown parameter names up front (atomic: apply nothing) so a typo
131
+ # like `gain` on a levelTOP fails loudly instead of being silently dropped.
132
+ unknown = [k for k in params if getattr(node.par, k, None) is None]
133
+ if unknown:
134
+ raise ValueError(
135
+ "Unknown parameter(s) on %s (%s): %s. "
136
+ "Use get_td_node_parameters to see the valid parameter names."
137
+ % (path, op_type(node), ", ".join(sorted(unknown)))
138
+ )
139
+ applied, failed = apply_parameters(node, params)
140
+ if failed:
141
+ raise ValueError(
142
+ "Could not set parameter(s) on %s (%s): %s "
143
+ "(wrong value type or out of range?). Applied: %s."
144
+ % (path, op_type(node), ", ".join(sorted(failed)), ", ".join(sorted(applied)) or "none")
145
+ )
124
146
  return node_detail(node)
125
147
 
126
148