avoid serializing field: null + docs

This commit is contained in:
lda
2026-05-19 22:13:43 +07:00 Verified
parent 8a345883d1
commit ce2b1a498c
14 changed files with 484 additions and 21 deletions
+93
View File
@@ -0,0 +1,93 @@
# lda rambles
that was surprisingly tuff response from our first user. it made me think about how we handle @node atm.
atm only one way is allowed: path. If you want to inbuilt a value; please use the the graph input.
our first user doesnt understand this. AT ALL.
this calls to several problems.
## dict use, instead of list[InputMap]
why dict, when we can:
```python
class InputMap(BaseModel, Generic[TypeT]):
key: SupportToValue[TypeT]
input: Path
```
same thing
```python
class OutputMap(BaseModel, Generic[TypeT]):
output: Pathexpr
key (give me a better key name): SupportToValue[TypeT]
```
look at this support to value, or use another fitting name.
what it does is:
Path("state.foo") -> TypeT
Literal(TypeT) -> TypeT
two modes that we currently need to PATCH IN. we do not PATCH wf_core if there isnt a greater problem.
### default arguments, and literal()
expand on literal, in wf_authoring we can have some Field bs:
```python
@node(
in_map = [
InputMap(dont support this either, use g.use()),
]
)
def baz(spam: InputT = "literal input", egg: Input2T = Path(do not support this.)):
...
```
so that means wf_authoring.node shouldnt do any field augmentation.
```python
g.use(
baz,
in_map = [
now were talking
]
)
```
this SHOULD support both path and literal.
## Context, of some sort
langgraph has 3 ways of storage, if you read tests/rewrite
lets copy that over:
```
According to https://docs.langchain.com/oss/python/concepts/context, there are three types:
| type | mut | lifetime |
| --- | --- | --- |
|static runtime (context) | static | single run |
|dynamic runtime (state) | mut | single run |
|dynamic cross-convo (store) | mut | cross-conversation |
now what the hell is store
## store
store is used in langgraph-demo for debugging. but it can be used for more things.
it saves every turn. every graph nodes. I use InMemoryStore, you can use psql store!
This allows for picking the work up again after a while for example.
a lot more versatility there.
```
now we 100% has state. we kinda have store if you skim it (trace), but trace is no store. and Context is patched in through state.
this can be done with support for async, because lowk store is going to give us superpowers.
## complaints come when user uses
not a lot from me rn.
+15
View File
@@ -353,6 +353,21 @@ be explicit, for example:
} }
``` ```
For node inputs:
- `in` is path mapping only, in source-to-destination order such as
`"input.url": "url"`.
- `with` is for static node-local values such as
`"value": "CLICKED"`.
- Do not put literal objects inside `in`. Invalid `use` step payloads should be
rejected, not silently treated as joins.
For strict MCP servers such as Playwright, unmapped optional tool arguments
should be omitted. If a trace shows optional keys being sent as `null`, capture
the capability name, the node trace `resolved_input`, and the upstream error;
that points at the workflow capability wrapper boundary rather than the raw MCP
tool.
## MCP Resources Or Prompts Are Missing ## MCP Resources Or Prompts Are Missing
First ask whether the upstream server actually supports them. First ask whether the upstream server actually supports them.
+90
View File
@@ -93,6 +93,75 @@ Important details:
- When saved with source bindings, concrete refs can be normalized to logical - When saved with source bindings, concrete refs can be normalized to logical
refs such as `demo.echo_tool`. refs such as `demo.echo_tool`.
## Mapping Shape
Draft maps are JSON objects whose keys and values are strings:
```ts
type InMap = Record<string, string>
type OutMap = Record<string, string>
```
Both maps are source-to-destination.
| Map | Key | Value | Example |
| --- | --- | --- | --- |
| `in` | graph source path | node-local input path | `"input.text": "message"` |
| `out` | node-local output path | graph state destination path | `"echoed": "state.echoed"` |
Graph source paths in `in` normally start with `input.`, `state.`, or
`context.`. Node-local paths do not use those prefixes; they are paths inside
the target capability's input or output payload.
For example:
```json
{
"in": {
"input.user.name": "user.name",
"state.job.title": "job.title"
},
"out": {
"user.age": "state.person.age",
"job.years": "state.experience.years"
}
}
```
Read that as:
- `input.user.name` -> local input `user.name`
- `state.job.title` -> local input `job.title`
- local output `user.age` -> `state.person.age`
- local output `job.years` -> `state.experience.years`
Do not reverse the direction. This is wrong:
```json
{
"in": {
"message": "input.text"
}
}
```
That asks the runtime to read from graph path `message` and write into a
node-local input field literally named `input.text`.
Do not put constants in `in`. This is also wrong:
```json
{
"in": {
"value": {
"value": "CLICKED"
}
}
}
```
Use `with` for static node-local values instead.
## Step Kinds ## Step Kinds
### `use` ### `use`
@@ -114,6 +183,27 @@ Calls a workflow capability.
Use this for normal node calls, including generated workflow wrappers around Use this for normal node calls, including generated workflow wrappers around
MCP tools and local `wf.std` capabilities. MCP tools and local `wf.std` capabilities.
`use` steps can also provide static node-local input values with `with`.
Use this for hardcoded strings, booleans, numbers, and small JSON values that
are part of the graph definition:
```json
{
"use": "wf.std.constant",
"with": {
"value": "CLICKED"
},
"out": {
"value": "state.wait_text"
}
}
```
Static values are not path mappings. Do not put `{"value": "CLICKED"}` inside
`in`. The `in` object only maps graph paths such as `input.url` or
`state.wait_text` to node-local input fields. Invalid draft step shapes are
rejected instead of silently compiling to `join`.
Generated MCP tool wrappers are intentionally naive. They normally expose both Generated MCP tool wrappers are intentionally naive. They normally expose both
`ok` and `error` outcomes, because MCP tool calls can report transport/provider `ok` and `error` outcomes, because MCP tool calls can report transport/provider
errors separately from useful output. Drafts should wire both outcomes: errors separately from useful output. Drafts should wire both outcomes:
+1
View File
@@ -42,6 +42,7 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
step.use, step.use,
id=step_id, id=step_id,
in_map=step.in_, in_map=step.in_,
input_values=step.with_,
out_map=step.out, out_map=step.out,
desc=step.desc, desc=step.desc,
) )
+53 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, Literal from typing import Any, Literal
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator
from wf_core.models.conditions import Condition from wf_core.models.conditions import Condition
@@ -15,9 +15,33 @@ STEP_KIND_KEYS = frozenset(
class DraftUseStep(BaseModel): class DraftUseStep(BaseModel):
"""Draft step that calls one externally resolvable workflow capability.""" """Draft step that calls one externally resolvable workflow capability."""
model_config = ConfigDict(extra="forbid", populate_by_name=True)
use: str use: str
in_: dict[str, str] = Field(default_factory=dict, alias="in") in_: dict[str, str] = Field(
out: dict[str, str] = Field(default_factory=dict) default_factory=dict,
alias="in",
description=(
"Source-to-destination map from graph paths to node-local input "
"paths. Example: {'input.text': 'message'}. Values must be strings; "
"use 'with' for literals."
),
)
with_: dict[str, Any] = Field(
default_factory=dict,
alias="with",
description=(
"Static node-local input values keyed by destination input field/path. "
"Example: {'value': 'CLICKED'}."
),
)
out: dict[str, str] = Field(
default_factory=dict,
description=(
"Source-to-destination map from node-local output paths to workflow "
"state destinations. Example: {'echoed': 'state.echoed'}."
),
)
desc: str | None = None desc: str | None = None
retry: int | None = Field(default=None, ge=0) retry: int | None = Field(default=None, ge=0)
timeout_seconds: int | None = Field(default=None, gt=0) timeout_seconds: int | None = Field(default=None, gt=0)
@@ -26,6 +50,8 @@ class DraftUseStep(BaseModel):
class DraftForeachPayload(BaseModel): class DraftForeachPayload(BaseModel):
"""Payload for one draft foreach step.""" """Payload for one draft foreach step."""
model_config = ConfigDict(extra="forbid", populate_by_name=True)
over: str over: str
as_: str = Field(alias="as") as_: str = Field(alias="as")
mode: Literal["serial", "parallel"] = "serial" mode: Literal["serial", "parallel"] = "serial"
@@ -35,12 +61,16 @@ class DraftForeachPayload(BaseModel):
class DraftForeachStep(BaseModel): class DraftForeachStep(BaseModel):
"""Draft step that delegates foreach construction to `WorkflowBuilder`.""" """Draft step that delegates foreach construction to `WorkflowBuilder`."""
model_config = ConfigDict(extra="forbid")
foreach: DraftForeachPayload foreach: DraftForeachPayload
class DraftInterruptPayload(BaseModel): class DraftInterruptPayload(BaseModel):
"""Payload for one draft interrupt step.""" """Payload for one draft interrupt step."""
model_config = ConfigDict(extra="forbid")
kind: str kind: str
request: dict[str, str] = Field(default_factory=dict) request: dict[str, str] = Field(default_factory=dict)
resume: dict[str, str] = Field(default_factory=dict) resume: dict[str, str] = Field(default_factory=dict)
@@ -50,18 +80,24 @@ class DraftInterruptPayload(BaseModel):
class DraftInterruptStep(BaseModel): class DraftInterruptStep(BaseModel):
"""Draft step that pauses execution and waits for resume input.""" """Draft step that pauses execution and waits for resume input."""
model_config = ConfigDict(extra="forbid")
interrupt: DraftInterruptPayload interrupt: DraftInterruptPayload
class DraftJoinStep(BaseModel): class DraftJoinStep(BaseModel):
"""Draft step that emits the current core join node.""" """Draft step that emits the current core join node."""
model_config = ConfigDict(extra="forbid")
join: JsonObject = Field(default_factory=dict) join: JsonObject = Field(default_factory=dict)
class DraftWhenPayload(BaseModel): class DraftWhenPayload(BaseModel):
"""Payload for one boolean draft decision.""" """Payload for one boolean draft decision."""
model_config = ConfigDict(extra="forbid", populate_by_name=True)
if_: Condition = Field(alias="if") if_: Condition = Field(alias="if")
then: str then: str
otherwise: str = "__end__" otherwise: str = "__end__"
@@ -70,12 +106,16 @@ class DraftWhenPayload(BaseModel):
class DraftWhenStep(BaseModel): class DraftWhenStep(BaseModel):
"""Draft step that delegates one boolean decision to `WorkflowBuilder.when`.""" """Draft step that delegates one boolean decision to `WorkflowBuilder.when`."""
model_config = ConfigDict(extra="forbid")
when: DraftWhenPayload when: DraftWhenPayload
class DraftChooseClause(BaseModel): class DraftChooseClause(BaseModel):
"""One ordered boolean clause in a draft choose decision.""" """One ordered boolean clause in a draft choose decision."""
model_config = ConfigDict(extra="forbid", populate_by_name=True)
if_: Condition = Field(alias="if") if_: Condition = Field(alias="if")
then: str then: str
@@ -83,6 +123,8 @@ class DraftChooseClause(BaseModel):
class DraftChoosePayload(BaseModel): class DraftChoosePayload(BaseModel):
"""Payload for an ordered first-true draft decision.""" """Payload for an ordered first-true draft decision."""
model_config = ConfigDict(extra="forbid")
clauses: list[DraftChooseClause] = Field(min_length=1) clauses: list[DraftChooseClause] = Field(min_length=1)
default: str = "__end__" default: str = "__end__"
@@ -90,12 +132,16 @@ class DraftChoosePayload(BaseModel):
class DraftChooseStep(BaseModel): class DraftChooseStep(BaseModel):
"""Draft step that delegates ordered decisions to `WorkflowBuilder.choose`.""" """Draft step that delegates ordered decisions to `WorkflowBuilder.choose`."""
model_config = ConfigDict(extra="forbid")
choose: DraftChoosePayload choose: DraftChoosePayload
class DraftMatchCase(BaseModel): class DraftMatchCase(BaseModel):
"""One ordered equality case in a draft match decision.""" """One ordered equality case in a draft match decision."""
model_config = ConfigDict(extra="forbid")
equals: Any equals: Any
then: str then: str
@@ -103,6 +149,8 @@ class DraftMatchCase(BaseModel):
class DraftMatchPayload(BaseModel): class DraftMatchPayload(BaseModel):
"""Payload for matching one graph value against ordered equality cases.""" """Payload for matching one graph value against ordered equality cases."""
model_config = ConfigDict(extra="forbid")
value: str value: str
cases: list[DraftMatchCase] = Field(min_length=1) cases: list[DraftMatchCase] = Field(min_length=1)
default: str = "__end__" default: str = "__end__"
@@ -111,6 +159,8 @@ class DraftMatchPayload(BaseModel):
class DraftMatchStep(BaseModel): class DraftMatchStep(BaseModel):
"""Draft step that delegates equality decisions to `WorkflowBuilder.match`.""" """Draft step that delegates equality decisions to `WorkflowBuilder.match`."""
model_config = ConfigDict(extra="forbid")
match: DraftMatchPayload match: DraftMatchPayload
+4
View File
@@ -84,6 +84,7 @@ class WorkflowBuilder:
*, *,
id: str | None = None, id: str | None = None,
in_map: MapArg | None = None, in_map: MapArg | None = None,
input_values: Mapping[str, Any] | None = None,
out_map: MapArg | None = None, out_map: MapArg | None = None,
desc: str | None = None, desc: str | None = None,
) -> NodeUse: ) -> NodeUse:
@@ -104,6 +105,7 @@ class WorkflowBuilder:
if in_map is None if in_map is None
else normalize_mapping(in_map) else normalize_mapping(in_map)
), ),
input_values=dict(input_values or {}),
out_map=( out_map=(
auto_output_map(spec, state_schema=normalized_state_schema) auto_output_map(spec, state_schema=normalized_state_schema)
if out_map is None if out_map is None
@@ -119,6 +121,7 @@ class WorkflowBuilder:
*, *,
id: str | None = None, id: str | None = None,
in_map: MapArg | None = None, in_map: MapArg | None = None,
input_values: Mapping[str, Any] | None = None,
out_map: MapArg | None = None, out_map: MapArg | None = None,
desc: str | None = None, desc: str | None = None,
) -> NodeUse: ) -> NodeUse:
@@ -135,6 +138,7 @@ class WorkflowBuilder:
node=name, node=name,
desc=desc, desc=desc,
in_map=normalize_mapping(in_map), in_map=normalize_mapping(in_map),
input_values=dict(input_values or {}),
out_map=normalize_mapping(out_map), out_map=normalize_mapping(out_map),
) )
self.nodes.append(node) self.nodes.append(node)
+23 -2
View File
@@ -14,8 +14,29 @@ class NodeUse(BaseModel):
type: Literal["node"] type: Literal["node"]
node: str node: str
desc: str | None = None desc: str | None = None
in_map: dict[str, str] = Field(default_factory=dict) in_map: dict[str, str] = Field(
out_map: dict[str, str] = Field(default_factory=dict) default_factory=dict,
description=(
"Map graph source paths to node-local input paths. Keys are paths "
"such as input.text, state.user.name, or context.item; values are "
"input fields/paths inside the node payload."
),
)
input_values: dict[str, object] = Field(
default_factory=dict,
description=(
"Static node-local input values keyed by destination input field/path. "
"Use this for graph-defined constants; use in_map only for graph paths."
),
)
out_map: dict[str, str] = Field(
default_factory=dict,
description=(
"Map node-local output paths to workflow state destinations. Keys "
"are output fields/paths inside the node payload; values must be "
"state.* destination paths."
),
)
retry: int | None = Field(default=None, ge=0) retry: int | None = Field(default=None, ge=0)
timeout_seconds: int | None = Field(default=None, gt=0) timeout_seconds: int | None = Field(default=None, gt=0)
+5
View File
@@ -33,6 +33,11 @@ def _resolve_node_execution(
frame = run.current_frame() frame = run.current_frame()
context_values = frame_context_values(frame) context_values = frame_context_values(frame)
resolved_input: dict[str, Any] = {} resolved_input: dict[str, Any] = {}
for destination_field, value in node.input_values.items():
try:
set_local_value(resolved_input, destination_field, value)
except LocalPathError as exc:
raise WorkflowExecutionError(str(exc)) from exc
for source_path, destination_field in node.in_map.items(): for source_path, destination_field in node.in_map.items():
value = safe_resolve_path( value = safe_resolve_path(
source_path, source_path,
+17
View File
@@ -38,6 +38,17 @@ def validate_node_use(
state_fields = set(workflow.state_schema.fields) state_fields = set(workflow.state_schema.fields)
input_root_fields = set(workflow.input_schema.properties) input_root_fields = set(workflow.input_schema.properties)
for destination_field in node.input_values:
destination_root = _local_root(destination_field)
if destination_root is None or (
destination_root != "." and destination_root not in input_fields
):
report.add(
ValidationIssueCode.INVALID_NODE_INPUT_FIELD,
f"nodes[{index}].input_values[{destination_field!r}]",
f"destination field {destination_field!r} is not declared in node input schema",
)
for source_path, destination_field in node.in_map.items(): for source_path, destination_field in node.in_map.items():
destination_root = _local_root(destination_field) destination_root = _local_root(destination_field)
if destination_root is None or ( if destination_root is None or (
@@ -63,6 +74,12 @@ def validate_node_use(
f"nodes[{index}].in_map", f"nodes[{index}].in_map",
"in_map has overlapping node-local input paths", "in_map has overlapping node-local input paths",
) )
if has_overlapping_paths([*node.input_values, *node.in_map.values()]):
report.add(
ValidationIssueCode.INVALID_NODE_INPUT_FIELD,
f"nodes[{index}].input_values",
"static input_values overlap with path-based in_map destinations",
)
for source_field, destination_path in node.out_map.items(): for source_field, destination_path in node.out_map.items():
source_root = _local_root(source_field) source_root = _local_root(source_field)
+4 -2
View File
@@ -126,14 +126,16 @@ def wrap_discovered_tool(
"tool_call_started", "tool_call_started",
connection_id=connection.id, connection_id=connection.id,
capability_id=f"{connection.id}.{tool.name}", capability_id=f"{connection.id}.{tool.name}",
payload={"input": payload.model_dump()}, payload={"input": payload.model_dump(exclude_unset=True)},
) )
) )
result = await adapter.call_tool( result = await adapter.call_tool(
connection=connection, connection=connection,
auth=auth, auth=auth,
tool_name=tool.name, tool_name=tool.name,
payload=payload.model_dump(), # Pydantic fills absent optional fields with None, but strict MCP
# servers such as Playwright distinguish omitted from explicit null.
payload=payload.model_dump(exclude_unset=True),
) )
if emit_event is not None: if emit_event is not None:
emit_event( emit_event(
+59
View File
@@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
from pydantic import ValidationError
from wf_artifacts.drafts import WorkflowDraft from wf_artifacts.drafts import WorkflowDraft
from wf_artifacts.drafts.api import compile_workflow_draft, validate_workflow_draft
from wf_artifacts.drafts.adapter import build_workflow_from_draft from wf_artifacts.drafts.adapter import build_workflow_from_draft
from wf_core import ConditionNode, NodeUse from wf_core import ConditionNode, NodeUse
@@ -29,6 +32,62 @@ def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
assert workflow.edges[0].to == "__end__" assert workflow.edges[0].to == "__end__"
def test_adapter_lowers_static_inputs_for_constant_like_steps() -> None:
draft = WorkflowDraft.model_validate(
{
"name": "constant",
"input_schema": {},
"state_schema": {"fields": {"message": {"type": "string"}}},
"output_schema": {},
"start": "constant",
"steps": {
"constant": {
"use": "wf.std.constant",
"with": {"value": "CLICKED"},
"out": {"value": "state.message"},
}
},
"routes": {"constant": {"ok": "__end__"}},
}
)
workflow = build_workflow_from_draft(draft)
node = workflow.nodes[0]
assert isinstance(node, NodeUse)
assert node.node == "wf.std.constant"
assert node.input_values["value"] == "CLICKED"
assert node.in_map == {}
def test_invalid_literal_input_map_does_not_fall_through_to_join() -> None:
draft = {
"name": "bad_constant",
"input_schema": {},
"state_schema": {"fields": {}},
"output_schema": {},
"start": "constant",
"steps": {
"constant": {
"use": "wf.std.constant",
"in": {"value": {"value": "CLICKED"}},
}
},
"routes": {"constant": {"ok": "__end__"}},
}
result = validate_workflow_draft(draft)
assert result["status"] == "invalid"
assert "steps.constant" in result["diagnostics"][0]["path"]
try:
compile_workflow_draft(draft)
except ValidationError:
pass
else: # pragma: no cover - kept explicit because silent join fallback was the bug.
raise AssertionError("invalid use step compiled instead of failing")
def test_adapter_lowers_when_step_through_builder() -> None: def test_adapter_lowers_when_step_through_builder() -> None:
draft = WorkflowDraft.model_validate( draft = WorkflowDraft.model_validate(
{ {
+49
View File
@@ -113,6 +113,55 @@ def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
assert run.state["rates"] == {"r_1": 0.0, "r_10": 0.1} assert run.state["rates"] == {"r_1": 0.0, "r_10": 0.1}
def test_static_input_values_are_merged_into_node_local_input() -> None:
workflow = Workflow(
name="static_input_values",
input_schema=SchemaRef.model_validate({"type": "object", "properties": {}}),
state_schema=StateSchema(fields={"message": StateField(type="string")}),
output_schema=SchemaRef(type="object", properties={}),
node_defs=[
NodeDef(
name="constant",
input_schema=SchemaRef.model_validate(
{
"type": "object",
"properties": {"value": {"type": "string"}},
"required": ["value"],
}
),
output_schema=SchemaRef.model_validate(
{
"type": "object",
"properties": {"value": {"type": "string"}},
"required": ["value"],
}
),
outcomes=["ok"],
)
],
start="constant",
nodes=[
NodeUse(
id="constant",
type="node",
node="constant",
input_values={"value": "CLICKED"},
out_map={"value": "state.message"},
)
],
edges=[Edge.model_validate({"from": "constant", "outcome": "ok", "to": END})],
)
run = execute_workflow(
workflow,
{},
{"constant": lambda payload, _ctx: {"outcome": "ok", "output": payload}},
)
assert run.trace[0].resolved_input["value"] == "CLICKED"
assert run.state["message"] == "CLICKED"
def _nested_mapping_workflow() -> Workflow: def _nested_mapping_workflow() -> Workflow:
return Workflow( return Workflow(
name="nested_mapping", name="nested_mapping",
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import asyncio
from typing import Any, cast
from wf_authoring import build_async_registry
from wf_core import RuntimeContext
from wf_mcp.capabilities import DiscoveredTool
from wf_mcp.models import AuthRecord, ConnectionConfig
from wf_mcp.sdk import BackendAdapter, ToolCallResult
from wf_mcp.workflow import wrap_discovered_tool
class RecordingAdapter:
"""Tiny adapter fake that records exactly what MCP payload would be sent."""
def __init__(self) -> None:
self.payloads: list[dict[str, Any]] = []
async def call_tool(
self,
connection: ConnectionConfig,
auth: AuthRecord | None,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult:
self.payloads.append(payload)
return ToolCallResult(outcome="ok", output={})
def test_discovered_tool_wrapper_omits_unset_optional_arguments() -> None:
adapter = RecordingAdapter()
spec = wrap_discovered_tool(
connection=ConnectionConfig(
id="playwright.default",
server="playwright",
account="default",
),
auth=None,
adapter=cast(BackendAdapter, adapter),
tool=DiscoveredTool(
name="browser_snapshot",
title=None,
description=None,
input_schema={
"type": "object",
"properties": {
"target": {"type": "string"},
"depth": {"type": "integer"},
},
},
output_schema={"type": "object", "properties": {}},
),
)
handler = build_async_registry(spec)[spec.name]
async def run_calls() -> None:
await handler({}, RuntimeContext(current_node_id="snapshot"))
await handler(
{"target": "main"},
RuntimeContext(current_node_id="snapshot"),
)
asyncio.run(run_calls())
assert adapter.payloads[0] == {}
assert adapter.payloads[1] == {"target": "main"}
+4 -14
View File
@@ -9,9 +9,7 @@
"metadata": { "metadata": {
"transport": "stdio", "transport": "stdio",
"command": "pnpx", "command": "pnpx",
"args": [ "args": ["@upstash/context7-mcp"],
"@upstash/context7-mcp"
],
"env": {} "env": {}
} }
}, },
@@ -23,10 +21,7 @@
"metadata": { "metadata": {
"transport": "stdio", "transport": "stdio",
"command": "pnpx", "command": "pnpx",
"args": [ "args": ["@playwright/mcp@latest"],
"@playwright/mcp@latest",
"--isolated"
],
"env": {} "env": {}
} }
}, },
@@ -38,9 +33,7 @@
"metadata": { "metadata": {
"transport": "stdio", "transport": "stdio",
"command": "pnpx", "command": "pnpx",
"args": [ "args": ["@modelcontextprotocol/server-everything"],
"@modelcontextprotocol/server-everything"
],
"env": {} "env": {}
} }
}, },
@@ -52,10 +45,7 @@
"metadata": { "metadata": {
"transport": "stdio", "transport": "stdio",
"command": "serena", "command": "serena",
"args": [ "args": ["start-mcp-server", "--project-from-cwd"],
"start-mcp-server",
"--project-from-cwd"
],
"env": {} "env": {}
} }
} }