diff --git a/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java b/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java index 9dc4ba6ba4..0578eb01fd 100644 --- a/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java +++ b/modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java @@ -16,6 +16,7 @@ import java.io.File; import java.nio.file.Path; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -42,6 +43,8 @@ public class OpenAPITask extends BaseTask { private List adoc; + private Boolean mcp; + /** * Creates an OpenAPI task. */ @@ -89,7 +92,11 @@ public void generate() throws Throwable { OpenAPI result = tool.generate(mainClass); var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList(); - for (var format : OpenAPIGenerator.Format.values()) { + var formats = EnumSet.allOf(OpenAPIGenerator.Format.class); + if (mcp != Boolean.TRUE) { + formats.remove(OpenAPIGenerator.Format.MCP); + } + for (var format : formats) { tool.export(result, format, Map.of("adoc", adocPath)) .forEach(output -> getLogger().info(" writing: " + output)); } @@ -154,6 +161,21 @@ public void setIncludes(@Nullable String includes) { this.includes = includes; } + /** + * Beta generate a mcp like file format. Disabled by default. + * + * @return Mcp. + */ + @Input + @org.gradle.api.tasks.Optional + public Boolean getMcp() { + return mcp; + } + + public void setMcp(Boolean mcp) { + this.mcp = mcp; + } + /** * Regular expression used to excludes route. Example: /web. * diff --git a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java index 35f8d41a1d..41d1bfc114 100644 --- a/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java +++ b/modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java @@ -12,6 +12,7 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -49,6 +50,9 @@ public class OpenAPIMojo extends BaseMojo { @Parameter(property = "openAPI.specVersion") private String specVersion; + @Parameter(property = "openAPI.mcp") + private Boolean mcp; + @Parameter private List adoc; @Override @@ -80,6 +84,10 @@ protected void doExecute(@NonNull List projects, @NonNull String m var result = tool.generate(mainClass); var adocPath = ofNullable(adoc).orElse(List.of()).stream().map(File::toPath).toList(); + var formats = EnumSet.allOf(OpenAPIGenerator.Format.class); + if (mcp != Boolean.TRUE) { + formats.remove(OpenAPIGenerator.Format.MCP); + } for (var format : OpenAPIGenerator.Format.values()) { tool.export(result, format, Map.of("adoc", adocPath)) .forEach(output -> getLog().info(" writing: " + output)); @@ -144,4 +152,12 @@ public List getAdoc() { public void setAdoc(List adoc) { this.adoc = adoc; } + + public Boolean getMcp() { + return mcp; + } + + public void setMcp(Boolean mcp) { + this.mcp = mcp; + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenApiSupport.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenApiSupport.java new file mode 100644 index 0000000000..7a20c63a26 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenApiSupport.java @@ -0,0 +1,41 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi; + +import static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import io.swagger.v3.oas.models.media.Schema; + +public class OpenApiSupport { + + protected final OpenAPIExt openapi; + + public OpenApiSupport(OpenAPIExt openapi) { + this.openapi = openapi; + } + + public Schema resolveSchema(Schema schema) { + if (schema.get$ref() != null) { + return resolveSchemaInternal(schema.get$ref()) + .orElseThrow(() -> new NoSuchElementException("Schema not found: " + schema.get$ref())); + } + return schema; + } + + protected Optional> resolveSchemaInternal(String name) { + var components = openapi.getComponents(); + if (components == null || components.getSchemas() == null) { + throw new NoSuchElementException("No schema found"); + } + if (name.startsWith(COMPONENTS_SCHEMAS_REF)) { + name = name.substring(COMPONENTS_SCHEMAS_REF.length()); + } + return Optional.ofNullable((Schema) components.getSchemas().get(name)); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java index c599382a7d..651e55f68c 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java @@ -5,7 +5,6 @@ */ package io.jooby.internal.openapi.asciidoc; -import static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF; import static java.util.Optional.ofNullable; import java.io.IOException; @@ -29,6 +28,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.internal.openapi.OpenAPIExt; +import io.jooby.internal.openapi.OpenApiSupport; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; import io.pebbletemplates.pebble.extension.AbstractExtension; @@ -45,7 +45,7 @@ import io.swagger.v3.oas.models.media.NumberSchema; import io.swagger.v3.oas.models.media.Schema; -public class AsciiDocContext { +public class AsciiDocContext extends OpenApiSupport { public static final BiConsumer> NOOP = (name, schema) -> {}; private ObjectMapper json; @@ -70,6 +70,7 @@ public class AsciiDocContext { } public AsciiDocContext(Path baseDir, ObjectMapper json, ObjectMapper yaml, OpenAPIExt openapi) { + super(openapi); this.json = json; this.yamlOpenApi = yaml; this.yamlOutput = newYamlOutput(); @@ -304,14 +305,6 @@ public String schemaType(Schema schema) { return Optional.ofNullable(resolved.getFormat()).orElse(resolved.getType()); } - public Schema resolveSchema(Schema schema) { - if (schema.get$ref() != null) { - return resolveSchemaInternal(schema.get$ref()) - .orElseThrow(() -> new NoSuchElementException("Schema not found: " + schema.get$ref())); - } - return schema; - } - public Object schemaProperties(Schema schema) { var resolved = resolveSchema(schema); if ("array".equals(resolved.getType())) { @@ -464,17 +457,6 @@ public Schema resolveSchema(String path) { return schema; } - private Optional> resolveSchemaInternal(String name) { - var components = openapi.getComponents(); - if (components == null || components.getSchemas() == null) { - throw new NoSuchElementException("No schema found"); - } - if (name.startsWith(COMPONENTS_SCHEMAS_REF)) { - name = name.substring(COMPONENTS_SCHEMAS_REF.length()); - } - return Optional.ofNullable((Schema) components.getSchemas().get(name)); - } - public PebbleEngine getEngine() { return engine; } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpContext.java new file mode 100644 index 0000000000..25095a8fd3 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpContext.java @@ -0,0 +1,200 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.mcp; + +import static io.jooby.SneakyThrows.throwingFunction; +import static java.util.Optional.ofNullable; + +import java.io.IOException; +import java.util.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.CaseFormat; +import io.jooby.MediaType; +import io.jooby.SneakyThrows; +import io.jooby.internal.openapi.OpenAPIExt; +import io.jooby.internal.openapi.OpenApiSupport; +import io.jooby.internal.openapi.OperationExt; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.tags.Tag; + +public class McpContext extends OpenApiSupport { + + static { + // type vs types difference in v30 vs v31 + System.setProperty(Schema.BIND_TYPE_AND_TYPES, Boolean.TRUE.toString()); + } + + private final ObjectMapper json = Json31.mapper(); + + public McpContext(OpenAPIExt openapi) { + super(openapi); + } + + public String generate() throws IOException { + var output = new LinkedHashMap(); + var tools = new LinkedHashMap(); + var templates = new LinkedHashMap(); + output.put("resourceTemplates", templates); + output.put("tools", tools); + + for (OperationExt operation : openapi.getOperations()) { + if (isTool(operation)) { + var inputSchema = inputSchema(operation); + var properties = inputSchema.getProperties(); + var parameters = ofNullable(operation.getParameters()).orElse(List.of()); + var required = new ArrayList(); + for (var parameter : parameters) { + var schema = cloneSchema(parameter.getSchema()); + var doc = parameter.getDescription(); + if (doc != null) { + schema.setDescription(doc); + } + if (parameter.getRequired() == Boolean.TRUE) { + required.add(parameter.getName()); + } + properties.put(parameter.getName(), schema); + } + var rsp = operation.getDefaultResponse(); + Schema outputSchema = null; + if (rsp != null) { + outputSchema = + schemaOf(rsp.getContent()) + .map(this::resolveSchema) + .map(throwingFunction(this::cloneSchema)) + .orElse(null); + } + inputSchema.setRequired(required); + var tool = + new McpTool( + toolId(operation.getOperationId()), + operation.getSummary(), + selfContained(inputSchema), + selfContained(outputSchema)); + tools.put(operation.getOperationId(), tool); + } else if (isResourceTemplate(operation)) { + var properties = new LinkedHashMap(); + var required = new ArrayList(); + for (var parameter : ofNullable(operation.getParameters()).orElse(List.of())) { + var schema = cloneSchema(parameter.getSchema()); + var doc = parameter.getDescription(); + if (doc != null) { + schema.setDescription(doc); + } + if (parameter.getRequired() == Boolean.TRUE) { + required.add(parameter.getName()); + } + properties.put(parameter.getName(), schema); + } + var uriSchema = + CaseFormat.UPPER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, + Optional.ofNullable(operation.getTags()).orElse(List.of()).stream() + .findFirst() + .orElse(operation.getController().name)); + var name = + operation.getGlobalTags().stream() + .map(Tag::getDescription) + .filter(Objects::nonNull) + .findFirst() + .orElse(operation.getPathSummary()); + var mimeType = operation.getProduces().stream().findFirst().orElse(MediaType.JSON); + var parameters = new Schema<>(); + parameters.setType("object"); + parameters.setProperties(properties); + parameters.setRequired(required); + templates.put( + operation.getOperationId(), + new McpResourceTemplate( + uriSchema + ":/" + operation.getPath(), + name, + operation.getDescription(), + mimeType, + parameters)); + } + // resource vs tool + } + return json.writer().withDefaultPrettyPrinter().writeValueAsString(output); + } + + private Schema selfContained(Schema in) { + if (in != null) { + if ("object".equals(in.getType())) { + if (in.getProperties() != null) { + Map properties = new LinkedHashMap<>(); + for (var entry : in.getProperties().entrySet()) { + var propertyIn = entry.getValue(); + var propertyOut = selfContained(cloneSchema(propertyIn)); + properties.put(entry.getKey(), propertyOut); + } + in.setProperties(properties); + return in; + } + } else if ("array".equals(in.getType())) { + var items = in.getItems(); + in.setItems(selfContained(resolveSchema(items))); + } + } + return in; + } + + private Schema inputSchema(OperationExt operation) { + var requestBody = operation.getRequestBody(); + Schema result = null; + if (requestBody != null) { + var content = requestBody.getContent(); + result = schemaOf(content).map(throwingFunction(this::cloneSchema)).orElse(null); + } + if (result == null) { + result = new Schema<>(); + result.setType("object"); + } + if (result.getProperties() == null) { + result.setProperties(new LinkedHashMap<>()); + } + return result; + } + + private Optional> schemaOf(Content content) { + if (content != null && !content.isEmpty()) { + return ofNullable(content.values().iterator().next().getSchema()); + } + return Optional.empty(); + } + + private Schema cloneSchema(Schema in) { + try { + var source = json.valueToTree(resolveSchema(in)); + var clone = json.treeToValue(source, Schema.class); + clone.setType(in.getType()); + clone.setTypes(in.getTypes()); + clone.setName(in.getName()); + return clone; + } catch (JsonProcessingException x) { + throw SneakyThrows.propagate(x); + } + } + + private boolean isTool(OperationExt operation) { + var parameters = ofNullable(operation.getParameters()).orElse(List.of()); + // No path, TODO: probably check extensions: like, x-tool + return parameters.stream().noneMatch(p -> p.getIn().equals("path")); + } + + private boolean isResourceTemplate(OperationExt operation) { + var parameters = ofNullable(operation.getParameters()).orElse(List.of()); + // No path, TODO: probably check extensions: like, x-resource-template + return operation.getMethod().equals("GET") + && parameters.stream().anyMatch(p -> p.getIn().equals("path")); + } + + private String toolId(String operationId) { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, operationId); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpResourceTemplate.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpResourceTemplate.java new file mode 100644 index 0000000000..317b93d6e6 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpResourceTemplate.java @@ -0,0 +1,11 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.mcp; + +import io.swagger.v3.oas.models.media.Schema; + +public record McpResourceTemplate( + String uriTemplate, String name, String description, String mimeType, Schema parameters) {} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpTool.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpTool.java new file mode 100644 index 0000000000..ec86d1950e --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/mcp/McpTool.java @@ -0,0 +1,11 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.mcp; + +import io.swagger.v3.oas.models.media.Schema; + +public record McpTool( + String name, String description, Schema inputSchema, Schema outputSchema) {} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java index b73fe20638..b61ed7ff83 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java @@ -23,6 +23,7 @@ import io.jooby.internal.openapi.*; import io.jooby.internal.openapi.asciidoc.AsciiDocContext; import io.jooby.internal.openapi.javadoc.JavaDocParser; +import io.jooby.internal.openapi.mcp.McpContext; import io.swagger.v3.core.util.*; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.PathItem; @@ -67,6 +68,16 @@ public enum Format { } }, + MCP { + @Override + @NonNull protected String toString( + @NonNull OpenAPIGenerator tool, + @NonNull OpenAPI result, + @NonNull Map options) { + return tool.toMcp(result); + } + }, + ADOC { @Override @NonNull protected String toString( @@ -379,6 +390,21 @@ private void defaults(String classname, String contextPath, OpenAPIExt openapi) } } + /** + * Generates a mcp version of the given model. + * + * @param openAPI Model. + * @return Mcp content. + */ + public @NonNull String toMcp(@NonNull OpenAPI openAPI) { + try { + var mcp = new McpContext((OpenAPIExt) openAPI); + return mcp.generate(); + } catch (IOException e) { + throw SneakyThrows.propagate(e); + } + } + /** * Generates an adoc version of the given model. * diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java index c11fb966d4..ab7296d627 100644 --- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIResult.java @@ -13,6 +13,7 @@ import io.jooby.SneakyThrows; import io.jooby.internal.openapi.OpenAPIExt; import io.jooby.internal.openapi.asciidoc.AsciiDocContext; +import io.jooby.internal.openapi.mcp.McpContext; import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.Yaml; import io.swagger.v3.parser.OpenAPIV3Parser; @@ -76,6 +77,10 @@ public String toJson(boolean validate) { if (failure != null) { throw failure; } + return json(validate); + } + + private String json(boolean validate) { try { String json = this.json.writerWithDefaultPrettyPrinter().writeValueAsString(openAPI); if (validate) { @@ -104,18 +109,7 @@ public String toAsciiDoc(Path index, boolean validate) { throw failure; } try { - String json = this.json.writerWithDefaultPrettyPrinter().writeValueAsString(openAPI); - if (validate) { - SwaggerParseResult result = new OpenAPIV3Parser().readContents(json); - if (result.getMessages().isEmpty()) { - return json; - } - throw new IllegalStateException( - "Invalid OpenAPI specification:\n\t- " - + String.join("\n\t- ", result.getMessages()).trim() - + "\n\n" - + json); - } + json(validate); var asciiDoc = new AsciiDocContext(index.getParent(), this.json, this.yaml, openAPI); return asciiDoc.generate(index); } catch (Exception x) { @@ -123,6 +117,23 @@ public String toAsciiDoc(Path index, boolean validate) { } } + public String toMcp() { + return toMcp(false); + } + + public String toMcp(boolean validate) { + if (failure != null) { + throw failure; + } + try { + json(validate); + var mcp = new McpContext(openAPI); + return mcp.generate(); + } catch (Exception x) { + throw SneakyThrows.propagate(x); + } + } + public static OpenAPIResult failure(RuntimeException failure) { var result = new OpenAPIResult(Json.mapper(), Yaml.mapper(), null); result.failure = failure; diff --git a/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java b/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java index d0ca32dd39..9f3cde1ede 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java +++ b/modules/jooby-openapi/src/test/java/issues/i3820/app/LibApi.java @@ -7,6 +7,7 @@ import java.util.List; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.annotation.*; import io.jooby.exception.NotFoundException; import issues.i3820.model.Author; @@ -79,10 +80,10 @@ public List searchBooks(@QueryParam String q) { @Path("/books") @Produces("application/json") public Page getBooksByTitle( - @QueryParam String title, @QueryParam int page, @QueryParam int size) { + @NonNull @QueryParam String title, @QueryParam Integer page, @QueryParam Integer size) { // Ensure we have sensible defaults if the user sends nothing - int pageNum = page > 0 ? page : 1; - int pageSize = size > 0 ? size : 20; + int pageNum = page != null ? page : 1; + int pageSize = size != null ? size : 20; // Ask the database for just this specific slice of data return library.findBooksByTitle(title, PageRequest.ofPage(pageNum).size(pageSize)); diff --git a/modules/jooby-openapi/src/test/java/issues/i3830/McpTest.java b/modules/jooby-openapi/src/test/java/issues/i3830/McpTest.java new file mode 100644 index 0000000000..7242e5888e --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3830/McpTest.java @@ -0,0 +1,509 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3830; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.jooby.openapi.OpenAPIResult; +import io.jooby.openapi.OpenAPITest; +import issues.i3820.app.AppLib; + +public class McpTest { + + @OpenAPITest(value = AppLib.class) + public void shouldGenerateMcpMetadata(OpenAPIResult result) { + assertThat(result.toMcp()) + .isEqualToIgnoringNewLines( + """ + { + "resourceTemplates" : { + "getBook" : { + "uriTemplate" : "library://library/books/{isbn}", + "name" : "The Public Front Desk of the library.", + "description" : "View the full information for a single specific book using its unique ISBN.", + "mimeType" : "application/json", + "parameters" : { + "properties" : { + "isbn" : { + "type" : "string", + "description" : "The unique ID from the URL (e.g., /books/978-3-16-148410-0)" + } + }, + "required" : [ "isbn" ] + } + } + }, + "tools" : { + "searchBooks" : { + "name" : "search_books", + "description" : "Quick Search", + "inputSchema" : { + "properties" : { + "q" : { + "type" : "string", + "description" : "The word or phrase to search for." + } + } + }, + "outputSchema" : { + "type" : "array", + "items" : { + "description" : "Represents a physical Book in our library.

This is the main item visitors look for. It holds details like the title, the actual text content, and who published it.", + "properties" : { + "isbn" : { + "type" : "string", + "description" : "The unique \\"barcode\\" for this book (ISBN). We use this to identify exactly which book edition we are talking about." + }, + "title" : { + "type" : "string", + "description" : "The name printed on the cover." + }, + "publicationDate" : { + "type" : "string", + "format" : "date", + "description" : "When this book was released to the public." + }, + "text" : { + "type" : "string", + "description" : "The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast." + }, + "type" : { + "type" : "string", + "description" : "Categorizes the item (e.g., is it a regular Book or a Magazine?).\\n - NOVEL: A fictional narrative story. Examples: \\"Pride and Prejudice\\", \\"Harry Potter\\", \\"Dune\\". These are creative works meant for entertainment or artistic expression.\\n - BIOGRAPHY: A written account of a real person's life. Examples: \\"Steve Jobs\\" by Walter Isaacson, \\"The Diary of a Young Girl\\". These are non-fiction historical records of an individual.\\n - TEXTBOOK: An educational book used for study. Examples: \\"Calculus: Early Transcendentals\\", \\"Introduction to Java Programming\\". These are designed for students and are often used as reference material in academic courses.\\n - MAGAZINE: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format.\\n - JOURNAL: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts.", + "enum" : [ "NOVEL", "BIOGRAPHY", "TEXTBOOK", "MAGAZINE", "JOURNAL" ] + }, + "publisher" : { + "description" : "A company that produces and sells books.", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64", + "description" : "The unique internal ID for this publisher. This is a number generated automatically by the system. Users usually don't need to memorize this, but it's used by the database to link books to their publishers." + }, + "name" : { + "type" : "string", + "description" : "The official business name of the publishing house. Example: \\"Penguin Random House\\" or \\"O'Reilly Media\\"." + } + } + }, + "authors" : { + "type" : "array", + "description" : "The list of people who wrote this book.", + "items" : { + "description" : "A person who writes books.", + "properties" : { + "ssn" : { + "type" : "string", + "description" : "The author's unique government ID (SSN)." + }, + "name" : { + "type" : "string", + "description" : "The full name of the author." + }, + "address" : { + "description" : "A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, Publishers, or Users.", + "properties" : { + "street" : { + "type" : "string", + "description" : "The specific street address. Includes the house number, street name, and apartment number if applicable. Example: \\"123 Maple Avenue, Apt 4B\\"." + }, + "city" : { + "type" : "string", + "description" : "The town, city, or municipality. Used for grouping authors by location or calculating shipping regions." + }, + "zip" : { + "type" : "string", + "description" : "The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., \\"02138\\") or contain letters (e.g., \\"K1A 0B1\\")." + } + } + } + } + }, + "uniqueItems" : true + } + } + } + } + }, + "getBooksByTitle" : { + "name" : "get_books_by_title", + "description" : "Browse Books (Paginated)", + "inputSchema" : { + "properties" : { + "title" : { + "type" : "string", + "description" : "The exact book title to filter by." + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "Which page number to load (defaults to 1)." + }, + "size" : { + "type" : "integer", + "format" : "int32", + "description" : "How many books to show per page (defaults to 20)." + } + }, + "required" : [ "title" ] + }, + "outputSchema" : { + "properties" : { + "content" : { + "type" : "array", + "items" : { + "description" : "Represents a physical Book in our library.

This is the main item visitors look for. It holds details like the title, the actual text content, and who published it.", + "properties" : { + "isbn" : { + "type" : "string", + "description" : "The unique \\"barcode\\" for this book (ISBN). We use this to identify exactly which book edition we are talking about." + }, + "title" : { + "type" : "string", + "description" : "The name printed on the cover." + }, + "publicationDate" : { + "type" : "string", + "format" : "date", + "description" : "When this book was released to the public." + }, + "text" : { + "type" : "string", + "description" : "The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast." + }, + "type" : { + "type" : "string", + "description" : "Categorizes the item (e.g., is it a regular Book or a Magazine?).\\n - NOVEL: A fictional narrative story. Examples: \\"Pride and Prejudice\\", \\"Harry Potter\\", \\"Dune\\". These are creative works meant for entertainment or artistic expression.\\n - BIOGRAPHY: A written account of a real person's life. Examples: \\"Steve Jobs\\" by Walter Isaacson, \\"The Diary of a Young Girl\\". These are non-fiction historical records of an individual.\\n - TEXTBOOK: An educational book used for study. Examples: \\"Calculus: Early Transcendentals\\", \\"Introduction to Java Programming\\". These are designed for students and are often used as reference material in academic courses.\\n - MAGAZINE: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format.\\n - JOURNAL: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts.", + "enum" : [ "NOVEL", "BIOGRAPHY", "TEXTBOOK", "MAGAZINE", "JOURNAL" ] + }, + "publisher" : { + "description" : "A company that produces and sells books.", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64", + "description" : "The unique internal ID for this publisher. This is a number generated automatically by the system. Users usually don't need to memorize this, but it's used by the database to link books to their publishers." + }, + "name" : { + "type" : "string", + "description" : "The official business name of the publishing house. Example: \\"Penguin Random House\\" or \\"O'Reilly Media\\"." + } + } + }, + "authors" : { + "type" : "array", + "description" : "The list of people who wrote this book.", + "items" : { + "description" : "A person who writes books.", + "properties" : { + "ssn" : { + "type" : "string", + "description" : "The author's unique government ID (SSN)." + }, + "name" : { + "type" : "string", + "description" : "The full name of the author." + }, + "address" : { + "description" : "A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, Publishers, or Users.", + "properties" : { + "street" : { + "type" : "string", + "description" : "The specific street address. Includes the house number, street name, and apartment number if applicable. Example: \\"123 Maple Avenue, Apt 4B\\"." + }, + "city" : { + "type" : "string", + "description" : "The town, city, or municipality. Used for grouping authors by location or calculating shipping regions." + }, + "zip" : { + "type" : "string", + "description" : "The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., \\"02138\\") or contain letters (e.g., \\"K1A 0B1\\")." + } + } + } + } + }, + "uniqueItems" : true + } + } + } + }, + "numberOfElements" : { + "type" : "integer", + "format" : "int32" + }, + "totalElements" : { + "type" : "integer", + "format" : "int64" + }, + "totalPages" : { + "type" : "integer", + "format" : "int64" + }, + "pageRequest" : { + "properties" : { + "page" : { + "type" : "integer", + "format" : "int64" + }, + "size" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "nextPageRequest" : { + "properties" : { + "page" : { + "type" : "integer", + "format" : "int64" + }, + "size" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "previousPageRequest" : { + "properties" : { + "page" : { + "type" : "integer", + "format" : "int64" + }, + "size" : { + "type" : "integer", + "format" : "int32" + } + } + } + } + } + }, + "addBook" : { + "name" : "add_book", + "description" : "Add New Book", + "inputSchema" : { + "description" : "Represents a physical Book in our library.

This is the main item visitors look for. It holds details like the title, the actual text content, and who published it.", + "properties" : { + "isbn" : { + "type" : "string", + "description" : "The unique \\"barcode\\" for this book (ISBN). We use this to identify exactly which book edition we are talking about." + }, + "title" : { + "type" : "string", + "description" : "The name printed on the cover." + }, + "publicationDate" : { + "type" : "string", + "format" : "date", + "description" : "When this book was released to the public." + }, + "text" : { + "type" : "string", + "description" : "The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast." + }, + "type" : { + "type" : "string", + "description" : "Categorizes the item (e.g., is it a regular Book or a Magazine?).\\n - NOVEL: A fictional narrative story. Examples: \\"Pride and Prejudice\\", \\"Harry Potter\\", \\"Dune\\". These are creative works meant for entertainment or artistic expression.\\n - BIOGRAPHY: A written account of a real person's life. Examples: \\"Steve Jobs\\" by Walter Isaacson, \\"The Diary of a Young Girl\\". These are non-fiction historical records of an individual.\\n - TEXTBOOK: An educational book used for study. Examples: \\"Calculus: Early Transcendentals\\", \\"Introduction to Java Programming\\". These are designed for students and are often used as reference material in academic courses.\\n - MAGAZINE: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format.\\n - JOURNAL: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts.", + "enum" : [ "NOVEL", "BIOGRAPHY", "TEXTBOOK", "MAGAZINE", "JOURNAL" ] + }, + "publisher" : { + "description" : "A company that produces and sells books.", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64", + "description" : "The unique internal ID for this publisher. This is a number generated automatically by the system. Users usually don't need to memorize this, but it's used by the database to link books to their publishers." + }, + "name" : { + "type" : "string", + "description" : "The official business name of the publishing house. Example: \\"Penguin Random House\\" or \\"O'Reilly Media\\"." + } + } + }, + "authors" : { + "type" : "array", + "description" : "The list of people who wrote this book.", + "items" : { + "description" : "A person who writes books.", + "properties" : { + "ssn" : { + "type" : "string", + "description" : "The author's unique government ID (SSN)." + }, + "name" : { + "type" : "string", + "description" : "The full name of the author." + }, + "address" : { + "description" : "A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, Publishers, or Users.", + "properties" : { + "street" : { + "type" : "string", + "description" : "The specific street address. Includes the house number, street name, and apartment number if applicable. Example: \\"123 Maple Avenue, Apt 4B\\"." + }, + "city" : { + "type" : "string", + "description" : "The town, city, or municipality. Used for grouping authors by location or calculating shipping regions." + }, + "zip" : { + "type" : "string", + "description" : "The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., \\"02138\\") or contain letters (e.g., \\"K1A 0B1\\")." + } + } + } + } + }, + "uniqueItems" : true + } + } + }, + "outputSchema" : { + "description" : "Represents a physical Book in our library.

This is the main item visitors look for. It holds details like the title, the actual text content, and who published it.", + "properties" : { + "isbn" : { + "type" : "string", + "description" : "The unique \\"barcode\\" for this book (ISBN). We use this to identify exactly which book edition we are talking about." + }, + "title" : { + "type" : "string", + "description" : "The name printed on the cover." + }, + "publicationDate" : { + "type" : "string", + "format" : "date", + "description" : "When this book was released to the public." + }, + "text" : { + "type" : "string", + "description" : "The full story or content of the book. Since this can be very long, we store it in a special way (Large Object) to keep the database fast." + }, + "type" : { + "type" : "string", + "description" : "Categorizes the item (e.g., is it a regular Book or a Magazine?).\\n - NOVEL: A fictional narrative story. Examples: \\"Pride and Prejudice\\", \\"Harry Potter\\", \\"Dune\\". These are creative works meant for entertainment or artistic expression.\\n - BIOGRAPHY: A written account of a real person's life. Examples: \\"Steve Jobs\\" by Walter Isaacson, \\"The Diary of a Young Girl\\". These are non-fiction historical records of an individual.\\n - TEXTBOOK: An educational book used for study. Examples: \\"Calculus: Early Transcendentals\\", \\"Introduction to Java Programming\\". These are designed for students and are often used as reference material in academic courses.\\n - MAGAZINE: A periodical publication intended for general readers. Examples: Time, National Geographic, Vogue. These contain various articles, are published frequently (weekly/monthly), and often have a glossy format.\\n - JOURNAL: A scholarly or professional publication. Examples: The New England Journal of Medicine, Harvard Law Review. These focus on academic research or trade news and are written by experts for other experts.", + "enum" : [ "NOVEL", "BIOGRAPHY", "TEXTBOOK", "MAGAZINE", "JOURNAL" ] + }, + "publisher" : { + "description" : "A company that produces and sells books.", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64", + "description" : "The unique internal ID for this publisher. This is a number generated automatically by the system. Users usually don't need to memorize this, but it's used by the database to link books to their publishers." + }, + "name" : { + "type" : "string", + "description" : "The official business name of the publishing house. Example: \\"Penguin Random House\\" or \\"O'Reilly Media\\"." + } + } + }, + "authors" : { + "type" : "array", + "description" : "The list of people who wrote this book.", + "items" : { + "description" : "A person who writes books.", + "properties" : { + "ssn" : { + "type" : "string", + "description" : "The author's unique government ID (SSN)." + }, + "name" : { + "type" : "string", + "description" : "The full name of the author." + }, + "address" : { + "description" : "A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, Publishers, or Users.", + "properties" : { + "street" : { + "type" : "string", + "description" : "The specific street address. Includes the house number, street name, and apartment number if applicable. Example: \\"123 Maple Avenue, Apt 4B\\"." + }, + "city" : { + "type" : "string", + "description" : "The town, city, or municipality. Used for grouping authors by location or calculating shipping regions." + }, + "zip" : { + "type" : "string", + "description" : "The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., \\"02138\\") or contain letters (e.g., \\"K1A 0B1\\")." + } + } + } + } + }, + "uniqueItems" : true + } + } + } + }, + "addAuthor" : { + "name" : "add_author", + "description" : "Add New Author", + "inputSchema" : { + "description" : "A person who writes books.", + "properties" : { + "ssn" : { + "type" : "string", + "description" : "The author's unique government ID (SSN)." + }, + "name" : { + "type" : "string", + "description" : "The full name of the author." + }, + "address" : { + "description" : "A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, Publishers, or Users.", + "properties" : { + "street" : { + "type" : "string", + "description" : "The specific street address. Includes the house number, street name, and apartment number if applicable. Example: \\"123 Maple Avenue, Apt 4B\\"." + }, + "city" : { + "type" : "string", + "description" : "The town, city, or municipality. Used for grouping authors by location or calculating shipping regions." + }, + "zip" : { + "type" : "string", + "description" : "The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., \\"02138\\") or contain letters (e.g., \\"K1A 0B1\\")." + } + } + } + } + }, + "outputSchema" : { + "description" : "A person who writes books.", + "properties" : { + "ssn" : { + "type" : "string", + "description" : "The author's unique government ID (SSN)." + }, + "name" : { + "type" : "string", + "description" : "The full name of the author." + }, + "address" : { + "description" : "A reusable way to store address details (Street, City, Zip). We can reuse this on Authors, Publishers, or Users.", + "properties" : { + "street" : { + "type" : "string", + "description" : "The specific street address. Includes the house number, street name, and apartment number if applicable. Example: \\"123 Maple Avenue, Apt 4B\\"." + }, + "city" : { + "type" : "string", + "description" : "The town, city, or municipality. Used for grouping authors by location or calculating shipping regions." + }, + "zip" : { + "type" : "string", + "description" : "The postal or zip code. Stored as text (String) rather than a number to support codes that start with zero (e.g., \\"02138\\") or contain letters (e.g., \\"K1A 0B1\\")." + } + } + } + } + } + } + } + } + """); + } +}