@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.
@@ -0,0 +1,128 @@
1
+ {
2
+ "version": "2.0.1",
3
+ "source": "https://www.cityjson.org/specs/2.0.1/",
4
+ "generated_at": "2025-12-20T12:19:52.522Z",
5
+ "chapters": [
6
+ {
7
+ "id": "profile-and-date",
8
+ "title": "Living Standard,\n 20 December 2025",
9
+ "path": "chapters/profile-and-date.md",
10
+ "order": 1,
11
+ "sections": []
12
+ },
13
+ {
14
+ "id": "cityjson-object",
15
+ "title": "1. CityJSON Object",
16
+ "path": "chapters/cityjson-object.md",
17
+ "order": 2,
18
+ "sections": []
19
+ },
20
+ {
21
+ "id": "the-different-city-objects",
22
+ "title": "2. The different City Objects",
23
+ "path": "chapters/the-different-city-objects.md",
24
+ "order": 3,
25
+ "sections": [
26
+ "2-1-attributes-for-all-city-objects",
27
+ "2-2-bridge",
28
+ "2-3-building",
29
+ "2-4-cityfurniture",
30
+ "2-5-cityobjectgroup",
31
+ "2-6-genericcityobject",
32
+ "2-7-landuse",
33
+ "2-8-otherconstruction",
34
+ "2-9-plantcover",
35
+ "2-10-solitaryvegetationobject",
36
+ "2-11-tinrelief",
37
+ "2-12-transportation",
38
+ "2-13-tunnel",
39
+ "2-14-waterbody"
40
+ ]
41
+ },
42
+ {
43
+ "id": "geometry-objects",
44
+ "title": "3. Geometry Objects",
45
+ "path": "chapters/geometry-objects.md",
46
+ "order": 4,
47
+ "sections": [
48
+ "3-1-coordinates-of-the-vertices",
49
+ "3-2-arrays-to-represent-boundaries",
50
+ "3-3-semantics-of-geometric-primitives",
51
+ "3-4-geometry-templates"
52
+ ]
53
+ },
54
+ {
55
+ "id": "transform-object",
56
+ "title": "4. Transform Object",
57
+ "path": "chapters/transform-object.md",
58
+ "order": 5,
59
+ "sections": []
60
+ },
61
+ {
62
+ "id": "metadata",
63
+ "title": "5. Metadata",
64
+ "path": "chapters/metadata.md",
65
+ "order": 6,
66
+ "sections": [
67
+ "5-1-geographicalextent-bbox",
68
+ "5-2-identifier",
69
+ "5-3-pointofcontact",
70
+ "5-4-referencedate",
71
+ "5-5-referencesystem-crs",
72
+ "5-6-title"
73
+ ]
74
+ },
75
+ {
76
+ "id": "appearance-object",
77
+ "title": "6. Appearance Object",
78
+ "path": "chapters/appearance-object.md",
79
+ "order": 7,
80
+ "sections": [
81
+ "6-1-geometry-object-having-material-s",
82
+ "6-2-geometry-object-having-texture-s",
83
+ "6-3-material-object",
84
+ "6-4-texture-object",
85
+ "6-5-vertices-texture-object"
86
+ ]
87
+ },
88
+ {
89
+ "id": "handling-large-files",
90
+ "title": "7. Handling large files",
91
+ "path": "chapters/handling-large-files.md",
92
+ "order": 8,
93
+ "sections": [
94
+ "7-1-decomposing-an-area-into-parts-tiles",
95
+ "7-2-text-sequences-and-streaming-with-cityjsonfeature"
96
+ ]
97
+ },
98
+ {
99
+ "id": "extensions",
100
+ "title": "8. Extensions",
101
+ "path": "chapters/extensions.md",
102
+ "order": 9,
103
+ "sections": [
104
+ "8-1-using-an-extension-in-a-cityjson-file",
105
+ "8-2-the-extension-file",
106
+ "8-3-case-1-adding-new-properties-at-the-root-of-a-document",
107
+ "8-4-case-2-defining-attributes-for-existing-city-objects",
108
+ "8-5-case-3-defining-a-new-semantic-object",
109
+ "8-6-case-4-creating-and-or-extending-new-city-objects",
110
+ "8-7-rules-to-follow-to-define-new-city-objects"
111
+ ]
112
+ },
113
+ {
114
+ "id": "cityjson-schemas",
115
+ "title": "9. CityJSON schemas",
116
+ "path": "chapters/cityjson-schemas.md",
117
+ "order": 10,
118
+ "sections": []
119
+ },
120
+ {
121
+ "id": "citygml-v30-implementation-details",
122
+ "title": "10. CityGML v3.0 implementation details",
123
+ "path": "chapters/citygml-v30-implementation-details.md",
124
+ "order": 11,
125
+ "sections": []
126
+ }
127
+ ]
128
+ }
package/src/config.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Configuration management
3
+ */
4
+
5
+ import { existsSync } from "node:fs";
6
+ import { dirname, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { DEFAULT_HTTP_PORT, SERVER_NAME, SERVER_VERSION } from "./constants.js";
9
+
10
+ export interface ServerConfig {
11
+ name: string;
12
+ version: string;
13
+ specsPath: string;
14
+ transport: "stdio" | "http";
15
+ httpPort: number;
16
+ }
17
+
18
+ /**
19
+ * Find project root by looking for pnpm-workspace.yaml
20
+ */
21
+ function findProjectRoot(startDir: string): string {
22
+ let current = startDir;
23
+ while (current !== "/" && !existsSync(resolve(current, "pnpm-workspace.yaml"))) {
24
+ current = dirname(current);
25
+ }
26
+ return current;
27
+ }
28
+
29
+ /**
30
+ * Load configuration from environment variables
31
+ */
32
+ export function loadConfig(): ServerConfig {
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
34
+ const projectRoot = findProjectRoot(__dirname);
35
+
36
+ const specsPath = process.env.CITYJSON_SPECS_PATH || resolve(projectRoot, "specs");
37
+ const transport = (process.env.TRANSPORT as "stdio" | "http") || "stdio";
38
+ const httpPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : DEFAULT_HTTP_PORT;
39
+
40
+ return {
41
+ name: SERVER_NAME,
42
+ version: SERVER_VERSION,
43
+ specsPath,
44
+ transport,
45
+ httpPort,
46
+ };
47
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Server constants
3
+ */
4
+
5
+ export const SERVER_NAME = "cityjson-spec-mcp-server";
6
+ export const SERVER_VERSION = "0.1.0";
7
+ export const DEFAULT_HTTP_PORT = 3000;
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point for MCP server
4
+ * Supports both stdio and HTTP transports
5
+ */
6
+
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { loadConfig } from "./config.js";
9
+ import { createServer } from "./server.js";
10
+
11
+ async function main(): Promise<void> {
12
+ const config = loadConfig();
13
+ const server = createServer(config);
14
+
15
+ if (config.transport === "stdio") {
16
+ const transport = new StdioServerTransport();
17
+ await server.connect(transport);
18
+ console.error("CityJSON Spec MCP Server running on stdio");
19
+ console.error(`Specs path: ${config.specsPath}`);
20
+ } else {
21
+ // HTTP transport - dynamically import to avoid loading express in stdio mode
22
+ const express = await import("express");
23
+ const { StreamableHTTPServerTransport } = await import(
24
+ "@modelcontextprotocol/sdk/server/streamableHttp.js"
25
+ );
26
+
27
+ const app = express.default();
28
+ app.use(express.json());
29
+
30
+ app.post("/mcp", async (req, res) => {
31
+ const transport = new StreamableHTTPServerTransport({
32
+ sessionIdGenerator: undefined,
33
+ enableJsonResponse: true,
34
+ });
35
+ res.on("close", () => transport.close());
36
+ await server.connect(transport);
37
+ await transport.handleRequest(req, res, req.body);
38
+ });
39
+
40
+ // Health check endpoint for Cloud Run
41
+ app.get("/", (_req, res) => {
42
+ res.status(200).json({ status: "ok", service: "cityjson-spec-mcp" });
43
+ });
44
+
45
+ app.get("/health", (_req, res) => {
46
+ res.status(200).json({ status: "ok" });
47
+ });
48
+
49
+ app.listen(config.httpPort, () => {
50
+ console.error(`CityJSON Spec MCP Server running on http://localhost:${config.httpPort}`);
51
+ console.error(`Specs path: ${config.specsPath}`);
52
+ });
53
+ }
54
+ }
55
+
56
+ main().catch((error) => {
57
+ console.error("Failed to start server:", error);
58
+ process.exit(1);
59
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Zod schemas for tool inputs
3
+ */
4
+
5
+ import { z } from "zod";
6
+
7
+ export const ReadOutlineInputSchema = z
8
+ .object({
9
+ include_sections: z
10
+ .boolean()
11
+ .default(true)
12
+ .describe("Include section headings within each chapter"),
13
+ })
14
+ .strict();
15
+
16
+ export const ReadChapterInputSchema = z
17
+ .object({
18
+ chapter: z
19
+ .string()
20
+ .min(1)
21
+ .describe("Chapter identifier (e.g., 'metadata', 'city-objects', 'geometry-objects')"),
22
+ })
23
+ .strict();
24
+
25
+ export type ReadOutlineInput = z.infer<typeof ReadOutlineInputSchema>;
26
+ export type ReadChapterInput = z.infer<typeof ReadChapterInputSchema>;
package/src/server.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * MCP Server initialization and tool registration
3
+ */
4
+
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import type { ServerConfig } from "./config.js";
7
+ import { ReadChapterInputSchema, ReadOutlineInputSchema } from "./schemas/tool-schemas.js";
8
+ import { ChapterIndexService } from "./services/chapter-index.js";
9
+ import { SpecLoader } from "./services/spec-loader.js";
10
+ import { handleReadChapter, handleReadOutline } from "./tools/index.js";
11
+
12
+ export function createServer(config: ServerConfig): McpServer {
13
+ const server = new McpServer(
14
+ { name: config.name, version: config.version },
15
+ { capabilities: { tools: {} } },
16
+ );
17
+
18
+ const indexService = new ChapterIndexService(config.specsPath);
19
+ const specLoader = new SpecLoader(config.specsPath);
20
+
21
+ // Register cityjson_read_spec_outline
22
+ server.tool(
23
+ "cityjson_read_spec_outline",
24
+ "Returns the table of contents for the CityJSON specification, including all chapters and their sections",
25
+ ReadOutlineInputSchema.shape,
26
+ async (params) => handleReadOutline(params, indexService),
27
+ );
28
+
29
+ // Register cityjson_read_spec_chapter
30
+ server.tool(
31
+ "cityjson_read_spec_chapter",
32
+ "Returns the full specification content for a specific chapter in Markdown format",
33
+ ReadChapterInputSchema.shape,
34
+ async (params) => handleReadChapter(params, specLoader, indexService),
35
+ );
36
+
37
+ return server;
38
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Chapter index service - manages the index.json file
3
+ */
4
+
5
+ import { readFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import type { Chapter, ChapterIndex } from "../types.js";
8
+
9
+ export class ChapterIndexService {
10
+ private index: ChapterIndex | null = null;
11
+ private specsPath: string;
12
+
13
+ constructor(specsPath: string) {
14
+ this.specsPath = specsPath;
15
+ }
16
+
17
+ async loadIndex(): Promise<ChapterIndex> {
18
+ if (this.index) {
19
+ return this.index;
20
+ }
21
+
22
+ const indexPath = join(this.specsPath, "index.json");
23
+ const content = await readFile(indexPath, "utf-8");
24
+ this.index = JSON.parse(content) as ChapterIndex;
25
+ return this.index;
26
+ }
27
+
28
+ async getChapterById(id: string): Promise<Chapter | undefined> {
29
+ const index = await this.loadIndex();
30
+ return index.chapters.find((c) => c.id === id);
31
+ }
32
+
33
+ async listChapterIds(): Promise<string[]> {
34
+ const index = await this.loadIndex();
35
+ return index.chapters.map((c) => c.id);
36
+ }
37
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Spec loader service - loads chapter Markdown files
3
+ */
4
+
5
+ import { readFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+
8
+ export class SpecLoader {
9
+ private cache: Map<string, string> = new Map();
10
+ private specsPath: string;
11
+
12
+ constructor(specsPath: string) {
13
+ this.specsPath = specsPath;
14
+ }
15
+
16
+ async loadChapter(chapterId: string): Promise<string> {
17
+ // Check cache first
18
+ const cached = this.cache.get(chapterId);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+
23
+ // Read from file
24
+ const chapterPath = join(this.specsPath, "chapters", `${chapterId}.md`);
25
+ const content = await readFile(chapterPath, "utf-8");
26
+
27
+ // Cache and return
28
+ this.cache.set(chapterId, content);
29
+ return content;
30
+ }
31
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tool registration index
3
+ */
4
+
5
+ export { handleReadOutline } from "./read-outline.js";
6
+ export { handleReadChapter } from "./read-chapter.js";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Implementation of cityjson_read_spec_chapter tool
3
+ */
4
+
5
+ import type { ReadChapterInput } from "../schemas/tool-schemas.js";
6
+ import type { ChapterIndexService } from "../services/chapter-index.js";
7
+ import type { SpecLoader } from "../services/spec-loader.js";
8
+ import type { ToolResult } from "../types.js";
9
+
10
+ /**
11
+ * Normalize chapter ID for flexible matching:
12
+ * - lowercase
13
+ * - trim whitespace
14
+ * - replace spaces/underscores with hyphens
15
+ * - remove special characters
16
+ */
17
+ function normalizeChapterId(id: string): string {
18
+ return id
19
+ .toLowerCase()
20
+ .trim()
21
+ .replace(/[\s_]+/g, "-") // spaces/underscores to hyphens
22
+ .replace(/[^a-z0-9-]/g, "") // remove special chars
23
+ .replace(/-+/g, "-") // collapse multiple hyphens
24
+ .replace(/^-|-$/g, ""); // trim leading/trailing hyphens
25
+ }
26
+
27
+ export async function handleReadChapter(
28
+ params: ReadChapterInput,
29
+ specLoader: SpecLoader,
30
+ indexService: ChapterIndexService,
31
+ ): Promise<ToolResult> {
32
+ const validChapters = await indexService.listChapterIds();
33
+ const normalizedInput = normalizeChapterId(params.chapter);
34
+
35
+ // Find matching chapter (exact or normalized match)
36
+ const matchedChapter = validChapters.find(
37
+ (chapterId) =>
38
+ chapterId === params.chapter || normalizeChapterId(chapterId) === normalizedInput,
39
+ );
40
+
41
+ if (!matchedChapter) {
42
+ return {
43
+ isError: true,
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: `Chapter '${
48
+ params.chapter
49
+ }' not found. Valid chapters: ${validChapters.join(", ")}`,
50
+ },
51
+ ],
52
+ };
53
+ }
54
+
55
+ try {
56
+ const content = await specLoader.loadChapter(matchedChapter);
57
+
58
+ return {
59
+ content: [{ type: "text", text: content }],
60
+ };
61
+ } catch (error) {
62
+ return {
63
+ isError: true,
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: `Error loading chapter '${matchedChapter}': ${
68
+ error instanceof Error ? error.message : "Unknown error"
69
+ }`,
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Implementation of cityjson_read_spec_outline tool
3
+ */
4
+
5
+ import type { ReadOutlineInput } from "../schemas/tool-schemas.js";
6
+ import type { ChapterIndexService } from "../services/chapter-index.js";
7
+ import type { ToolResult } from "../types.js";
8
+
9
+ export async function handleReadOutline(
10
+ params: ReadOutlineInput,
11
+ indexService: ChapterIndexService,
12
+ ): Promise<ToolResult> {
13
+ try {
14
+ const index = await indexService.loadIndex();
15
+
16
+ const result = {
17
+ version: index.version,
18
+ total_chapters: index.chapters.length,
19
+ chapters: params.include_sections
20
+ ? index.chapters
21
+ : index.chapters.map((c) => ({
22
+ id: c.id,
23
+ title: c.title,
24
+ order: c.order,
25
+ })),
26
+ };
27
+
28
+ return {
29
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
30
+ };
31
+ } catch (error) {
32
+ return {
33
+ isError: true,
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `Error loading specification index: ${
38
+ error instanceof Error ? error.message : "Unknown error"
39
+ }`,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ }
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Type definitions for mcp-server
3
+ */
4
+
5
+ export interface Chapter {
6
+ id: string;
7
+ title: string;
8
+ path: string;
9
+ order: number;
10
+ sections: string[];
11
+ }
12
+
13
+ export interface ChapterIndex {
14
+ version: string;
15
+ source: string;
16
+ generated_at: string;
17
+ chapters: Chapter[];
18
+ }
19
+
20
+ export interface ToolResult {
21
+ content: Array<{ type: "text"; text: string }>;
22
+ isError?: boolean;
23
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { resolve } from "node:path";
2
+ import { defineConfig } from "vite";
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ lib: {
7
+ entry: resolve(__dirname, "src/index.ts"),
8
+ formats: ["es"],
9
+ fileName: "index",
10
+ },
11
+ rollupOptions: {
12
+ external: [
13
+ // Node.js built-ins
14
+ /^node:/,
15
+ "http",
16
+ "http2",
17
+ "https",
18
+ "stream",
19
+ "crypto",
20
+ "fs",
21
+ "path",
22
+ "url",
23
+ "util",
24
+ "buffer",
25
+ "events",
26
+ "net",
27
+ "tls",
28
+ "os",
29
+ "zlib",
30
+ "child_process",
31
+ // External packages - externalize all dependencies
32
+ "@modelcontextprotocol/sdk",
33
+ /^@modelcontextprotocol\/sdk\/.*/,
34
+ "express",
35
+ "zod",
36
+ /^@hono\/.*/,
37
+ "hono",
38
+ ],
39
+ },
40
+ target: "node20",
41
+ outDir: "dist",
42
+ // Don't minify for Node.js
43
+ minify: false,
44
+ // Disable SSR-related optimizations
45
+ ssr: true,
46
+ },
47
+ });