@cityjson/cj-mcp 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, resolve, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { z } from "zod";
8
+ import { readFile } from "node:fs/promises";
9
+ const SERVER_NAME = "cityjson-spec-mcp-server";
10
+ const SERVER_VERSION = "0.1.0";
11
+ const DEFAULT_HTTP_PORT = 3e3;
12
+ function findProjectRoot(startDir) {
13
+ let current = startDir;
14
+ while (current !== "/" && !existsSync(resolve(current, "pnpm-workspace.yaml"))) {
15
+ current = dirname(current);
16
+ }
17
+ return current;
18
+ }
19
+ function loadConfig() {
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const projectRoot = findProjectRoot(__dirname);
22
+ const specsPath = process.env.CITYJSON_SPECS_PATH || resolve(projectRoot, "specs");
23
+ const transport = process.env.TRANSPORT || "stdio";
24
+ const httpPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : DEFAULT_HTTP_PORT;
25
+ return {
26
+ name: SERVER_NAME,
27
+ version: SERVER_VERSION,
28
+ specsPath,
29
+ transport,
30
+ httpPort
31
+ };
32
+ }
33
+ const ReadOutlineInputSchema = z.object({
34
+ include_sections: z.boolean().default(true).describe("Include section headings within each chapter")
35
+ }).strict();
36
+ const ReadChapterInputSchema = z.object({
37
+ chapter: z.string().min(1).describe("Chapter identifier (e.g., 'metadata', 'city-objects', 'geometry-objects')")
38
+ }).strict();
39
+ class ChapterIndexService {
40
+ index = null;
41
+ specsPath;
42
+ constructor(specsPath) {
43
+ this.specsPath = specsPath;
44
+ }
45
+ async loadIndex() {
46
+ if (this.index) {
47
+ return this.index;
48
+ }
49
+ const indexPath = join(this.specsPath, "index.json");
50
+ const content = await readFile(indexPath, "utf-8");
51
+ this.index = JSON.parse(content);
52
+ return this.index;
53
+ }
54
+ async getChapterById(id) {
55
+ const index = await this.loadIndex();
56
+ return index.chapters.find((c) => c.id === id);
57
+ }
58
+ async listChapterIds() {
59
+ const index = await this.loadIndex();
60
+ return index.chapters.map((c) => c.id);
61
+ }
62
+ }
63
+ class SpecLoader {
64
+ cache = /* @__PURE__ */ new Map();
65
+ specsPath;
66
+ constructor(specsPath) {
67
+ this.specsPath = specsPath;
68
+ }
69
+ async loadChapter(chapterId) {
70
+ const cached = this.cache.get(chapterId);
71
+ if (cached) {
72
+ return cached;
73
+ }
74
+ const chapterPath = join(this.specsPath, "chapters", `${chapterId}.md`);
75
+ const content = await readFile(chapterPath, "utf-8");
76
+ this.cache.set(chapterId, content);
77
+ return content;
78
+ }
79
+ }
80
+ async function handleReadOutline(params, indexService) {
81
+ try {
82
+ const index = await indexService.loadIndex();
83
+ const result = {
84
+ version: index.version,
85
+ total_chapters: index.chapters.length,
86
+ chapters: params.include_sections ? index.chapters : index.chapters.map((c) => ({
87
+ id: c.id,
88
+ title: c.title,
89
+ order: c.order
90
+ }))
91
+ };
92
+ return {
93
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
94
+ };
95
+ } catch (error) {
96
+ return {
97
+ isError: true,
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: `Error loading specification index: ${error instanceof Error ? error.message : "Unknown error"}`
102
+ }
103
+ ]
104
+ };
105
+ }
106
+ }
107
+ function normalizeChapterId(id) {
108
+ return id.toLowerCase().trim().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
109
+ }
110
+ async function handleReadChapter(params, specLoader, indexService) {
111
+ const validChapters = await indexService.listChapterIds();
112
+ const normalizedInput = normalizeChapterId(params.chapter);
113
+ const matchedChapter = validChapters.find(
114
+ (chapterId) => chapterId === params.chapter || normalizeChapterId(chapterId) === normalizedInput
115
+ );
116
+ if (!matchedChapter) {
117
+ return {
118
+ isError: true,
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: `Chapter '${params.chapter}' not found. Valid chapters: ${validChapters.join(", ")}`
123
+ }
124
+ ]
125
+ };
126
+ }
127
+ try {
128
+ const content = await specLoader.loadChapter(matchedChapter);
129
+ return {
130
+ content: [{ type: "text", text: content }]
131
+ };
132
+ } catch (error) {
133
+ return {
134
+ isError: true,
135
+ content: [
136
+ {
137
+ type: "text",
138
+ text: `Error loading chapter '${matchedChapter}': ${error instanceof Error ? error.message : "Unknown error"}`
139
+ }
140
+ ]
141
+ };
142
+ }
143
+ }
144
+ function createServer(config) {
145
+ const server = new McpServer(
146
+ { name: config.name, version: config.version },
147
+ { capabilities: { tools: {} } }
148
+ );
149
+ const indexService = new ChapterIndexService(config.specsPath);
150
+ const specLoader = new SpecLoader(config.specsPath);
151
+ server.tool(
152
+ "cityjson_read_spec_outline",
153
+ "Returns the table of contents for the CityJSON specification, including all chapters and their sections",
154
+ ReadOutlineInputSchema.shape,
155
+ async (params) => handleReadOutline(params, indexService)
156
+ );
157
+ server.tool(
158
+ "cityjson_read_spec_chapter",
159
+ "Returns the full specification content for a specific chapter in Markdown format",
160
+ ReadChapterInputSchema.shape,
161
+ async (params) => handleReadChapter(params, specLoader, indexService)
162
+ );
163
+ return server;
164
+ }
165
+ async function main() {
166
+ const config = loadConfig();
167
+ const server = createServer(config);
168
+ if (config.transport === "stdio") {
169
+ const transport = new StdioServerTransport();
170
+ await server.connect(transport);
171
+ console.error("CityJSON Spec MCP Server running on stdio");
172
+ console.error(`Specs path: ${config.specsPath}`);
173
+ } else {
174
+ const express = await import("express");
175
+ const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
176
+ const app = express.default();
177
+ app.use(express.json());
178
+ app.post("/mcp", async (req, res) => {
179
+ const transport = new StreamableHTTPServerTransport({
180
+ sessionIdGenerator: void 0,
181
+ enableJsonResponse: true
182
+ });
183
+ res.on("close", () => transport.close());
184
+ await server.connect(transport);
185
+ await transport.handleRequest(req, res, req.body);
186
+ });
187
+ app.get("/", (_req, res) => {
188
+ res.status(200).json({ status: "ok", service: "cityjson-spec-mcp" });
189
+ });
190
+ app.get("/health", (_req, res) => {
191
+ res.status(200).json({ status: "ok" });
192
+ });
193
+ app.listen(config.httpPort, () => {
194
+ console.error(`CityJSON Spec MCP Server running on http://localhost:${config.httpPort}`);
195
+ console.error(`Specs path: ${config.specsPath}`);
196
+ });
197
+ }
198
+ }
199
+ main().catch((error) => {
200
+ console.error("Failed to start server:", error);
201
+ process.exit(1);
202
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@cityjson/cj-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for querying CityJSON specification chapters",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "cj-mcp": "dist/index.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/cityjson/cj-mcp"
13
+ },
14
+ "homepage": "https://github.com/cityjson/cj-mcp",
15
+ "bugs": {
16
+ "url": "https://github.com/cityjson/cj-mcp/issues"
17
+ },
18
+ "license": "MIT",
19
+ "keywords": ["mcp", "cityjson", "model-context-protocol", "3d-city-models"],
20
+ "scripts": {
21
+ "build": "vite build",
22
+ "dev": "vite build --watch",
23
+ "start:stdio": "node dist/index.js",
24
+ "start:http": "TRANSPORT=http node dist/index.js"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.0",
28
+ "zod": "^3.25.0",
29
+ "express": "^4.21.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "^4.17.21",
33
+ "@types/node": "^22.10.2",
34
+ "vite": "^6.0.0",
35
+ "vite-node": "^2.0.0"
36
+ }
37
+ }
@@ -0,0 +1,186 @@
1
+ ## 6\. Appearance Object[](#appearance-object)
2
+
3
+ Both textures and materials are supported in CityJSON, and the same mechanisms used in CityGML are reused, so the conversion back-and-forth is easy. The material is represented with the [X3D](http://www.web3d.org/documents/specifications/19775-1/V3.2/Part01/components/shape.html#Material) specifications, as is the case for CityGML. For the texture, the [COLLADA standard](https://www.khronos.org/collada/) is reused, as is the case for CityGML. However:
4
+
5
+ * the CityGML class `GeoreferencedTexture` is not supported.
6
+
7
+ * the CityGML class `TexCoordGen` is not supported, ie one must specify the UV coordinates in the texture files.
8
+
9
+ * the major difference is that in CityGML each Material/Texture object keeps a list of the primitives using it, while in CityJSON it is the opposite: if a primitive has a Material/Texture then it is stated with the primitive (with a link to it).
10
+
11
+
12
+ An Appearance Object is a JSON object that
13
+
14
+ * **may** have one member with the name `"materials"`, whose value is an array of Material Objects.
15
+
16
+ * **may** have one member with the name `"textures"`, whose value is an array of Texture Objects.
17
+
18
+ * **may** have one member with the name `"vertices-texture"`, whose value is an array of coordinates of each so-called UV vertex of the city model.
19
+
20
+ * **may** have one member with the name `"default-theme-texture"`, whose value is the name of the default theme for the appearance (a string). This can be used if geometries have more than one textures, so that a viewer displays the default one.
21
+
22
+ * **may** have one member with the name `"default-theme-material"`, whose value is the name of the default theme for the material (a string). This can be used if geometries have more than one textures, so that a viewer displays the default one.
23
+
24
+
25
+ "appearance": {
26
+ "materials": \[\],
27
+ "textures":\[\],
28
+ "vertices-texture": \[\],
29
+ "default-theme-texture": "myDefaultTheme1",
30
+ "default-theme-material": "myDefaultTheme2"
31
+ }
32
+
33
+ ### 6.1. Geometry Object having material(s)[](#geometry-object-having-material-s)
34
+
35
+ Each surface in a Geometry Object can have one or more materials assigned to it. To store the material of a surface, a Geometry Object may have a member `"material"`. The value of this member is a collection of key-value pairs, where the key is the _theme_ of the material, and the value is one JSON object that **must** contain either:
36
+
37
+ * one member `"values"`. The value is a hierarchy of arrays with integers. Each integer refers to the position (0-based) in the `"materials"` member of the `"appearance"` member of the CityJSON object. If a surface has no material, then `null` should be used in the array. The depth of the array depends on the Geometry object, and is equal to the depth of the `"boundary"` array minus 2, because each surface (`[[]]`) gets one material.
38
+
39
+ * one member `"value"`. The value is one integer referring to the position (0-based) in the `"materials"` member of the `"appearance"` member of the CityJSON object. This is used because often the materials are used to colour full objects, and repetition of materials is not necessary.
40
+
41
+
42
+ In the following example, the Solid has 4 surfaces, and there are 2 themes ("irradiation" and "irradiation-2"). These could represent, for instance, the different colours based on different scenarios of an solar irradiation analysis. Notice that the last surface gets no material (for both themes), thus `null` is used.
43
+
44
+ {
45
+ "type": "Solid",
46
+ "lod": "2.1",
47
+ "boundaries": \[
48
+ \[ \[\[0, 3, 2, 1\]\], \[\[4, 5, 6, 7\]\], \[\[0, 1, 5, 4\]\], \[\[1, 2, 6, 5\]\] \]
49
+ \],
50
+ "material": {
51
+ "irradiation": {
52
+ "values": \[\[0, 0, 1, null\]\]
53
+ },
54
+ "irradiation-2": {
55
+ "values": \[\[2, 2, 1, null\]\]
56
+ }
57
+ }
58
+ }
59
+
60
+ ### 6.2. Geometry Object having texture(s)[](#geometry-object-having-texture-s)
61
+
62
+ To store the texture(s) of a surface, a Geometry Object may have a member with the name `"texture"`. Its value is a collection of key-value pairs, where the key is the _theme_ of the textures, and the value is one JSON object that must contain one member `"values"`, which is a hierarchy of arrays with integers. For each ring of each surface, the first value refers to the position (0-based) in the `"textures"` member of the `"appearance"` member of the CityJSON object. The other indices refer to the UV positions of the corresponding vertices (as listed in the `"boundaries"` member of the geometry). Therefore, each array representing a ring has one more value than the number of vertices in the ring.
63
+
64
+ The depth of the array depends on the Geometry object, and is equal to the depth of the `"boundary"` array.
65
+
66
+ In the following example, the Solid has 4 surfaces, and there are 2 themes: "winter-textures" and "summer-textures" could for instance represent the textures during winter and summer. Notice that the last 2 surfaces of the first theme gets no texture, thus the value `null` is used.
67
+
68
+ {
69
+ "type": "Solid",
70
+ "lod": "2.2",
71
+ "boundaries": \[
72
+ \[ \[\[0, 3, 2, 1\]\], \[\[4, 5, 6, 7\]\], \[\[0, 1, 5, 4\]\], \[\[1, 2, 6, 5\]\] \]
73
+ \],
74
+ "texture": {
75
+ "winter-textures": {
76
+ "values": \[
77
+ \[ \[\[0, 10, 23, 22, 21\]\], \[\[0, 1, 2, 6, 5\]\], \[\[null\]\], \[\[null\]\] \]
78
+ \]
79
+ },
80
+ "summer-textures": {
81
+ "values": \[
82
+ \[
83
+ \[\[1, 10, 23, 22, 21\]\],
84
+ \[\[1, 1, 2, 6, 5\]\],
85
+ \[\[1, 66, 12, 64, 5\]\],
86
+ \[\[2, 99, 21, 16, 25\]\]
87
+ \]
88
+ \]
89
+ }
90
+ }
91
+ }
92
+
93
+ ### 6.3. Material Object[](#material-object)
94
+
95
+ A Material Object:
96
+
97
+ * **must** have one member with the name `"name"`, whose value is a string identifying the material.
98
+
99
+ * **may** have the following members (their meaning is explained [there](http://www.web3d.org/documents/specifications/19775-1/V3.2/Part01/components/shape.html#Material)):
100
+
101
+ 1. `"ambientIntensity"`. The value is a number between 0.0 and 1.0.
102
+
103
+ 2. `"diffuseColor"`. The value is an array with 3 numbers between 0.0 and 1.0 (RGB colour).
104
+
105
+ 3. `"emissiveColor"`. The value is an array with 3 numbers between 0.0 and 1.0 (RGB colour).
106
+
107
+ 4. `"specularColor"`. The value is an array with 3 numbers between 0.0 and 1.0 (RGB colour).
108
+
109
+ 5. `"shininess"`. The whose value is a number between 0.0 and 1.0.
110
+
111
+ 6. `"transparency"`. The value is a number between 0.0 and 1.0 (1.0 being completely transparent).
112
+
113
+ 7. `"isSmooth"`. The value is a Boolean value, is defined in CityGML as a hint for normal interpolation. If this boolean flag is set to true, vertex normals should be used for shading (Gouraud shading). Otherwise, normals should be constant for a surface patch (flat shading).
114
+
115
+
116
+ > **Note:** If only `"name"` is defined for the Material Object, then it is up to the application that reads the CityJSON file to attach a material definition to the `"name"`. This might not always be possible. Therefore, it is advised to define as many from the optional members as needed for fully displaying the material.
117
+
118
+ "materials": \[
119
+ {
120
+ "name": "roofandground",
121
+ "ambientIntensity": 0.2000,
122
+ "diffuseColor": \[0.9000, 0.1000, 0.7500\],
123
+ "emissiveColor": \[0.9000, 0.1000, 0.7500\],
124
+ "specularColor": \[0.9000, 0.1000, 0.7500\],
125
+ "shininess": 0.2,
126
+ "transparency": 0.5,
127
+ "isSmooth": false
128
+ },
129
+ {
130
+ "name": "wall",
131
+ "ambientIntensity": 0.4000,
132
+ "diffuseColor": \[0.1000, 0.1000, 0.9000\],
133
+ "emissiveColor": \[0.1000, 0.1000, 0.9000\],
134
+ "specularColor": \[0.9000, 0.1000, 0.7500\],
135
+ "shininess": 0.0,
136
+ "transparency": 0.5,
137
+ "isSmooth": true
138
+ }
139
+ \]
140
+
141
+ ### 6.4. Texture Object[](#texture-object)
142
+
143
+ A Texture Object:
144
+
145
+ * **must** have one member with the name `"type"`. The value is a string with either "PNG" or "JPG" as value.
146
+
147
+ * **must** have one member with the name `"image"`. The value is a string with the name of the file. This file can be a URL (eg `"http://www.someurl.org/filename.jpg"`), a relative path (eg `"appearances/myroof.jpg"`), or an absolute path (eg `"/home/elvis/mycityjson/appearances/myroof.jpg"`).
148
+
149
+ * **may** have one member with the name `"wrapMode"`. The value can be any of the following: `"none"`, `"wrap"`, `"mirror"`, `"clamp"`, or `"border"`.
150
+
151
+ * **may** have one member with the name `"textureType"`. The value can be any of the following: `"unknown"`, `"specific"`, or `"typical"`.
152
+
153
+ * **may** have one member with the name `"borderColor"`. The value is an array with 4 numbers between 0.0 and 1.0 (RGBA colour).
154
+
155
+
156
+ "textures": \[
157
+ {
158
+ "type": "PNG",
159
+ "image": "http://www.someurl.org/filename.jpg"
160
+ },
161
+ {
162
+ "type": "JPG",
163
+ "image": "appearances/myroof.jpg",
164
+ "wrapMode": "wrap",
165
+ "textureType": "unknown",
166
+ "borderColor": \[0.0, 0.1, 0.2, 1.0\]
167
+ }
168
+ \]
169
+
170
+ ### 6.5. Vertices-texture Object[](#vertices-texture-object)
171
+
172
+ An Appearance Object may have one member with the name `"vertices-texture"`. Its value is an array of the _(u,v)_ coordinates of the vertices used for texturing surfaces. Their position in this array (0-based) is used by the `"texture"` member of the Geometry Objects.
173
+
174
+ * the array of vertices may be empty.
175
+
176
+ * one vertex must be an array with exactly 2 values, representing the _(u,v)_ coordinates.
177
+
178
+ * vertices may be repeated
179
+
180
+
181
+ "vertices-texture": \[
182
+ \[0.0, 0.5\],
183
+ \[1.0, 0.0\],
184
+ \[1.0, 1.0\],
185
+ \[0.0, 1.0\]
186
+ \]
@@ -0,0 +1,7 @@
1
+ ## 10\. CityGML v3.0 implementation details[](#citygml-v30-implementation-details)
2
+
3
+ CityJSON v2.0 is a partial implementation of the [CityGML v3.0 data model](https://docs.ogc.org/is/20-010/20-010.html), although not all extension modules have been implemented. CityJSON v2.0 consistently implements actions #1, #2, and #3 of the profiling mechanism specified in [Section 2.1 of the CityGML v3.0 Conceptual Model](https://docs.ogc.org/is/20-010/20-010.html#toc10).
4
+
5
+ The details of which modules are supported are available at [https://www.cityjson.org/citygml/v30/](https://www.cityjson.org/citygml/v30/).
6
+
7
+ * * *
@@ -0,0 +1,62 @@
1
+ ## 1\. CityJSON Object[](#cityjson-object)
2
+
3
+ A CityJSON object represents one 3D city model of a given area, this model may contain features of different types, as defined in the CityGML data model.
4
+
5
+ A CityJSON object:
6
+
7
+ * is a JSON object.
8
+
9
+ * **must** have one member with the name `"type"`. The value must be `"CityJSON"`.
10
+
11
+ * **must** have one member with the name `"version"`. The value must be a string with the version (X.Y) of the CityJSON object. Observe that while schemas can have a version with patch version (X.Y.Z), a CityJSON object points only to the minor version (X.Y), and for validation the latest schema of that minor version should be used.
12
+
13
+ * **must** have one member with the name `"transform"`. The value is a JSON object describing how to _decompress_ the integer coordinates of the geometries to obtain real-world coordinates. See [§ 4 Transform Object](#transform-object) for details.
14
+
15
+ * **must** have one member with the name `"CityObjects"`. The value of this member is a collection of key-value pairs, where the key is the ID of the object, and the value is one City Object. The ID of a City Object should be unique (within one CityJSON Object). See [§ 2 The different City Objects](#the-different-city-objects) for details.
16
+
17
+ * **must** have one member with the name `"vertices"`. The value is an array representing the coordinates of each vertex of the city model. See [§ 3.1 Coordinates of the vertices](#coordinates-of-the-vertices).
18
+
19
+ * **may** have one member with the name `"metadata"`. The value may be a JSON object describing the coordinate reference system used, the extent of the dataset, its creator, etc. See [§ 5 Metadata](#metadata) for details.
20
+
21
+ * **may** have one member with the name `"extensions"`, which is used if there are Extensions used in the file. See [§ 8 Extensions](#extensions) for details.
22
+
23
+ * **may** have one member with the name `"appearance"`. The value may contain JSON objects representing the textures and/or materials of surfaces. See [§ 6 Appearance Object](#appearance-object) for details.
24
+
25
+ * **may** have one member with the name `"geometry-templates"`, the value is a JSON object containing the templates that can be reused by different City Objects (usually for trees). This is equivalent to the concept of "implicit geometries" in CityGML. See [§ 3.4 Geometry templates](#geometry-templates) for details.
26
+
27
+ * **may** have other members, and their value is not prescribed. Because these are not standard members in CityJSON, they might be ignored by parsers.
28
+
29
+
30
+ > **Note:** **Suggested convention:** A file containing one CityJSON object may have the extension `'.city.json'`
31
+
32
+ The minimal valid CityJSON object is:
33
+
34
+ {
35
+ "type": "CityJSON",
36
+ "version": "2.0",
37
+ "transform": {
38
+ "scale": \[1.0, 1.0, 1.0\],
39
+ "translate": \[0.0, 0.0, 0.0\]
40
+ },
41
+ "CityObjects": {},
42
+ "vertices": \[\]
43
+ }
44
+
45
+ An "empty" but complete CityJSON object will look like this:
46
+
47
+ {
48
+ "type": "CityJSON",
49
+ "version": "2.0",
50
+ "extensions": {},
51
+ "transform": {
52
+ "scale": \[1.0, 1.0, 1.0\],
53
+ "translate": \[0.0, 0.0, 0.0\]
54
+ },
55
+ "metadata": {},
56
+ "CityObjects": {},
57
+ "vertices": \[\],
58
+ "appearance": {},
59
+ "geometry-templates": {}
60
+ }
61
+
62
+ > **Note:** While the order of the CityJSON member values should preferably be as above, not all JSON generators support this, therefore the order is not prescribed.
@@ -0,0 +1,3 @@
1
+ ## 9\. CityJSON schemas[](#cityjson-schemas)
2
+
3
+ The [JSON schemas](https://json-schema.org/) of the specifications are publicly available at [https://cityjson.org/schemas/](https://cityjson.org/schemas/).