Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- [#53](https://github.com/portofcontext/pctx/issues/53) Improved code generation support for tools with no input schema or all optional input schemas

### Fixed

## [v0.4.3] - 2026-01-27
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions crates/pctx/src/commands/mcp/dev/renderers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,23 +480,27 @@ fn render_tool_detail(f: &mut Frame, app: &App, area: Rect) {
"Input Type:",
Style::default().fg(SECONDARY).add_modifier(Modifier::BOLD),
)]));
lines.push(Line::from(format!(" {}", tool.input_signature)));
if let Some(i) = &tool.input_signature() {
lines.push(Line::from(format!(" {i}")));
} else {
lines.push(Line::from("void"));
}
lines.push(Line::from(""));

// Output type
lines.push(Line::from(vec![Span::styled(
"Output Type:",
Style::default().fg(SECONDARY).add_modifier(Modifier::BOLD),
)]));
lines.push(Line::from(format!(" {}", tool.output_signature)));
lines.push(Line::from(format!(" {}", tool.output_signature())));
lines.push(Line::from(""));

// TypeScript types
lines.push(Line::from(vec![Span::styled(
"TypeScript Definition:",
Style::default().fg(TERTIARY).add_modifier(Modifier::BOLD),
)]));
for line in tool.types.lines() {
for line in tool.types().lines() {
lines.push(Line::from(format!(" {line}")));
}

Expand Down
2 changes: 1 addition & 1 deletion crates/pctx_code_mode/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ categories = ["development-tools", "api-bindings"]
[dependencies]
# local
pctx_config = { version = "^0.1.3", path = "../pctx_config" }
pctx_codegen = { version = "^0.1.1", path = "../pctx_codegen" }
pctx_codegen = { version = "^0.2.0", path = "../pctx_codegen" }
pctx_executor = { version = "^0.1.2", path = "../pctx_executor" }
pctx_code_execution_runtime = { version = "^0.1.3", path = "../pctx_code_execution_runtime" }

Expand Down
28 changes: 13 additions & 15 deletions crates/pctx_code_mode/src/code_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ impl CodeMode {
Tool::new_mcp(
&mcp_tool.name,
mcp_tool.description.map(String::from),
input_schema,
Some(input_schema),
output_schema,
)
.map_err(|e| {
Expand Down Expand Up @@ -220,17 +220,15 @@ impl CodeMode {
}

// convert callback config into tool
let input_schema = if let Some(i) = &callback.input_schema {
serde_json::from_value::<pctx_codegen::RootSchema>(json!(i)).map_err(|e| {
Error::Message(format!(
"Failed parsing inputSchema as json schema for tool `{}`: {e}",
&callback.name
))
})?
} else {
// TODO: better empty input schema support
serde_json::from_value::<pctx_codegen::RootSchema>(json!({})).unwrap()
};
let input_schema = serde_json::from_value::<Option<pctx_codegen::RootSchema>>(json!(
&callback.input_schema
))
.map_err(|e| {
Error::Message(format!(
"Failed parsing inputSchema as json schema for tool `{}`: {e}",
&callback.name
))
})?;
let output_schema = if let Some(o) = &callback.output_schema {
Some(
serde_json::from_value::<pctx_codegen::RootSchema>(json!(o)).map_err(|e| {
Expand Down Expand Up @@ -372,9 +370,9 @@ impl CodeMode {
name: t.fn_name.clone(),
description: t.description.clone(),
},
input_type: t.input_signature.clone(),
output_type: t.output_signature.clone(),
types: t.types.clone(),
input_type: t.input_signature().unwrap_or_default(),
output_type: t.output_signature(),
types: t.types(),
}));
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/pctx_codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pctx_codegen"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
license = "MIT"
description = "Code generation utilities for pctx"
Expand Down
104 changes: 71 additions & 33 deletions crates/pctx_codegen/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::debug;

use crate::{CodegenResult, case::Case, generate_docstring, typegen::generate_types_new};
use crate::{
CodegenResult,
case::Case,
generate_docstring,
typegen::{TypegenResult, generate_types},
};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolSet {
Expand Down Expand Up @@ -53,23 +58,22 @@ namespace {namespace} {{
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub fn_name: String,
pub description: Option<String>,
pub input_schema: RootSchema,
pub output_schema: Option<RootSchema>,
pub variant: ToolVariant,

pub fn_name: String,
pub input_signature: String,
pub output_signature: String,
pub types: String,
pub input_schema: Option<RootSchema>,
pub output_schema: Option<RootSchema>,

pub variant: ToolVariant,
input_type: Option<TypegenResult>,
output_type: Option<TypegenResult>,
}

impl Tool {
pub fn new_mcp(
name: &str,
description: Option<String>,
input: RootSchema,
input: Option<RootSchema>,
output: Option<RootSchema>,
) -> CodegenResult<Self> {
Self::_new(name, description, input, output, ToolVariant::Mcp)
Expand All @@ -78,7 +82,7 @@ impl Tool {
pub fn new_callback(
name: &str,
description: Option<String>,
input: RootSchema,
input: Option<RootSchema>,
output: Option<RootSchema>,
) -> CodegenResult<Self> {
Self::_new(name, description, input, output, ToolVariant::Callback)
Expand All @@ -87,7 +91,7 @@ impl Tool {
fn _new(
name: &str,
description: Option<String>,
input: RootSchema,
input: Option<RootSchema>,
output: Option<RootSchema>,
variant: ToolVariant,
) -> CodegenResult<Self> {
Expand All @@ -97,15 +101,16 @@ impl Tool {
"Generating Typescript interface for tool: '{name}' -> function {fn_name}",
);

let input_types = generate_types_new(input.clone(), &format!("{fn_name}Input"))?;
let mut type_defs = input_types.types;
let output_signature = if let Some(o) = output.clone() {
let output_types = generate_types_new(o, &format!("{fn_name}Output"))?;
type_defs = format!("{type_defs}\n\n{}", output_types.types);
output_types.type_signature
let input_type = if let Some(i) = &input {
Some(generate_types(i.clone(), &format!("{fn_name}Input"))?)
} else {
debug!("No output type listed, falling back on `any`");
"any".to_string()
None
};

let output_type = if let Some(o) = output.clone() {
Some(generate_types(o, &format!("{fn_name}Output"))?)
} else {
None
};

Ok(Self {
Expand All @@ -114,59 +119,92 @@ impl Tool {
input_schema: input,
output_schema: output,
fn_name,
input_signature: input_types.type_signature,
output_signature,
types: type_defs,
input_type,
output_type,
variant,
})
}

pub fn input_signature(&self) -> Option<String> {
// No input schema -> no params for the generated function
self.input_type.as_ref().map(|i| i.type_signature.clone())
}

pub fn output_signature(&self) -> String {
// No output schema -> usually means not documented so output type fallback is `any`, not `void`
self.output_type
.as_ref()
.map(|o| o.type_signature.clone())
.unwrap_or("any".into())
}

pub fn types(&self) -> String {
let mut type_defs = String::new();
if let Some(i) = &self.input_type {
type_defs = i.types.clone();
}
if let Some(o) = &self.output_type {
type_defs = format!("{type_defs}\n\n{}", &o.types);
}

type_defs
}

pub fn fn_signature(&self, include_types: bool) -> String {
let docstring_content = self.description.clone().unwrap_or_default();

let types = if include_types && !self.types.is_empty() {
format!("{}\n\n", &self.types)
} else {
String::new()
let mut types = self.types();
if include_types && !types.is_empty() {
types = format!("{types}\n\n");
}

let params = match &self.input_type {
Some(i) if i.all_optional => format!("input: {} = {{}}", &i.type_signature),
Some(i) => format!("input: {}", &i.type_signature),
None => String::default(),
};

format!(
"{types}{docstring}\nexport async function {fn_name}(input: {input}): Promise<{output}>",
"{types}{docstring}\nexport async function {fn_name}({params}): Promise<{output}>",
docstring = generate_docstring(&docstring_content),
fn_name = &self.fn_name,
input = &self.input_signature,
output = &self.output_signature,
output = &self.output_signature(),
)
}

pub fn fn_impl(&self, toolset_name: &str) -> String {
let arguments = self
.input_schema
.as_ref()
.map(|_| format!("arguments: input,"))
.unwrap_or_default();
match self.variant {
ToolVariant::Mcp => {
format!(
"{fn_sig} {{
return await callMCPTool<{output}>({{
serverName: {name},
toolName: {tool},
arguments: input,
{arguments}
}});
}}",
fn_sig = self.fn_signature(true),
name = json!(toolset_name),
tool = json!(&self.name),
output = &self.output_signature,
output = &self.output_signature(),
)
}
ToolVariant::Callback => {
format!(
"{fn_sig} {{
return await invokeCallback<{output}>({{
id: {id},
arguments: input,
{arguments}
}});
}}",
fn_sig = self.fn_signature(true),
id = json!(format!("{toolset_name}.{}", &self.name)),
output = &self.output_signature,
output = &self.output_signature(),
)
}
}
Expand Down
Loading