@precepts/mcp-server 1.0.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/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ # Licensing
2
+
3
+ This repository uses a split-license model. Different parts of the
4
+ repository are covered by different licenses:
5
+
6
+ ## Documentation Content - CC BY-SA 4.0
7
+
8
+ Standards content is maintained in a separate repository
9
+ (precepts-dev/standards) and licensed under the Creative Commons
10
+ Attribution-ShareAlike 4.0 International License.
11
+
12
+ See LICENSE-CONTENT for details.
13
+
14
+ ## Source Code - AGPL 3.0
15
+
16
+ All source code files including `packages/`, `.github/workflows/`,
17
+ configuration files (*.ts, *.js, *.json), and any other programming
18
+ any other programming language files are licensed under the GNU Affero
19
+ General Public License v3.0.
20
+
21
+ See LICENSE-CODE for details.
22
+
23
+ ## Why Two Licenses?
24
+
25
+ - The **content** license (CC BY-SA 4.0) ensures standards are freely
26
+ usable by everyone - individuals, companies, and AI tools - while
27
+ requiring attribution and that derivatives remain open.
28
+
29
+ - The **code** license (AGPL 3.0) ensures the tooling remains open
30
+ source while protecting against proprietary forks being deployed
31
+ as competing services without sharing modifications.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+ import { z } from "zod";
7
+ import { loadStandards, searchStandards } from "./standards.js";
8
+ const require_ = createRequire(import.meta.url);
9
+ // Resolve docs root: prefer env var, then locate @precepts/standards via its exported schema file
10
+ const DOCS_ROOT = process.env.PRECEPTS_DOCS_ROOT ??
11
+ path.join(path.dirname(require_.resolve("@precepts/standards/schema/standards.schema.json")), "..", "standards");
12
+ const server = new McpServer({
13
+ name: "precepts-standards",
14
+ version: "0.1.0",
15
+ });
16
+ // Load standards at startup
17
+ const standards = loadStandards(DOCS_ROOT);
18
+ // ── Tool: list_standards ──────────────────────────────────────────
19
+ server.tool("list_standards", "List all available standards. Optionally filter by domain, status, or category.", {
20
+ domain: z
21
+ .string()
22
+ .optional()
23
+ .describe("Filter by domain (e.g., integration, product, ux, project)"),
24
+ status: z
25
+ .string()
26
+ .optional()
27
+ .describe("Filter by status (DRAFT, MANDATORY, RECOMMENDED, DEPRECATED)"),
28
+ category: z
29
+ .string()
30
+ .optional()
31
+ .describe("Filter by category (e.g., format, protocol, security)"),
32
+ documentType: z
33
+ .string()
34
+ .optional()
35
+ .describe("Filter by document type (standard, guideline, governance, best-practice)"),
36
+ }, async ({ domain, status, category, documentType }) => {
37
+ let filtered = standards;
38
+ if (domain)
39
+ filtered = filtered.filter((s) => s.domain === domain);
40
+ if (status)
41
+ filtered = filtered.filter((s) => s.metadata.status === status);
42
+ if (category)
43
+ filtered = filtered.filter((s) => s.metadata.category === category);
44
+ if (documentType)
45
+ filtered = filtered.filter((s) => s.metadata.documentType === documentType);
46
+ const listing = filtered.map((s) => ({
47
+ identifier: s.metadata.identifier,
48
+ name: s.metadata.name,
49
+ version: s.metadata.version,
50
+ status: s.metadata.status,
51
+ domain: s.domain,
52
+ documentType: s.metadata.documentType ?? "N/A",
53
+ category: s.metadata.category ?? "N/A",
54
+ summary: s.metadata.machine_summary ?? "",
55
+ }));
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: JSON.stringify(listing, null, 2),
61
+ },
62
+ ],
63
+ };
64
+ });
65
+ // ── Tool: get_standard ────────────────────────────────────────────
66
+ server.tool("get_standard", "Retrieve the full content of a specific standard by its identifier (e.g., INTG-STD-001).", {
67
+ identifier: z.string().describe("The standard identifier (e.g., INTG-STD-001)"),
68
+ }, async ({ identifier }) => {
69
+ const std = standards.find((s) => s.metadata.identifier.toLowerCase() === identifier.toLowerCase());
70
+ if (!std) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `Standard "${identifier}" not found. Use list_standards to see available standards.`,
76
+ },
77
+ ],
78
+ isError: true,
79
+ };
80
+ }
81
+ const header = Object.entries(std.metadata)
82
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
83
+ .join("\n");
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: `--- Metadata ---\n${header}\n\n--- Content ---\n${std.content}`,
89
+ },
90
+ ],
91
+ };
92
+ });
93
+ // ── Tool: search_standards ────────────────────────────────────────
94
+ server.tool("search_standards", "Full-text search across all standards. Returns matching standards with their metadata.", {
95
+ query: z.string().describe("Search query (searches identifier, name, summary, and body)"),
96
+ domain: z.string().optional().describe("Limit search to a specific domain"),
97
+ status: z.string().optional().describe("Limit search to a specific status"),
98
+ }, async ({ query, domain, status }) => {
99
+ const results = searchStandards(standards, query, { domain, status });
100
+ const listing = results.map((s) => ({
101
+ identifier: s.metadata.identifier,
102
+ name: s.metadata.name,
103
+ domain: s.domain,
104
+ status: s.metadata.status,
105
+ summary: s.metadata.machine_summary ?? "",
106
+ excerpt: s.content.slice(0, 300) + (s.content.length > 300 ? "..." : ""),
107
+ }));
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: results.length === 0
113
+ ? `No standards found matching "${query}".`
114
+ : JSON.stringify(listing, null, 2),
115
+ },
116
+ ],
117
+ };
118
+ });
119
+ // ── Resources: each standard as a resource ────────────────────────
120
+ for (const std of standards) {
121
+ server.resource(std.metadata.identifier, `standards://${std.metadata.identifier}`, {
122
+ description: std.metadata.name,
123
+ mimeType: "text/markdown",
124
+ }, async () => ({
125
+ contents: [
126
+ {
127
+ uri: `standards://${std.metadata.identifier}`,
128
+ mimeType: "text/markdown",
129
+ text: std.content,
130
+ },
131
+ ],
132
+ }));
133
+ }
134
+ // ── Resource: standards index ─────────────────────────────────────
135
+ server.resource("standards-index", "standards://index", {
136
+ description: "Index of all available standards with metadata",
137
+ mimeType: "application/json",
138
+ }, async () => ({
139
+ contents: [
140
+ {
141
+ uri: "standards://index",
142
+ mimeType: "application/json",
143
+ text: JSON.stringify(standards.map((s) => ({
144
+ identifier: s.metadata.identifier,
145
+ name: s.metadata.name,
146
+ version: s.metadata.version,
147
+ status: s.metadata.status,
148
+ domain: s.domain,
149
+ documentType: s.metadata.documentType,
150
+ category: s.metadata.category,
151
+ })), null, 2),
152
+ },
153
+ ],
154
+ }));
155
+ // ── Start server ──────────────────────────────────────────────────
156
+ async function main() {
157
+ const transport = new StdioServerTransport();
158
+ await server.connect(transport);
159
+ console.error(`Precepts MCP server running. Loaded ${standards.length} standard(s) from ${DOCS_ROOT}`);
160
+ }
161
+ main().catch((err) => {
162
+ console.error("Failed to start MCP server:", err);
163
+ process.exit(1);
164
+ });
@@ -0,0 +1,32 @@
1
+ export interface StandardMetadata {
2
+ identifier: string;
3
+ name: string;
4
+ version: string;
5
+ status: "DRAFT" | "MANDATORY" | "RECOMMENDED" | "DEPRECATED";
6
+ domain?: string;
7
+ documentType?: "standard" | "guideline" | "governance" | "best-practice";
8
+ category?: string;
9
+ appliesTo?: string[];
10
+ dependsOn?: string[];
11
+ supersedes?: string;
12
+ machine_summary?: string;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface Standard {
16
+ metadata: StandardMetadata;
17
+ content: string;
18
+ filePath: string;
19
+ domain: string;
20
+ }
21
+ /**
22
+ * Load and parse all standards documents from the docs directory.
23
+ */
24
+ export declare function loadStandards(docsRoot: string): Standard[];
25
+ /**
26
+ * Simple full-text search across standards.
27
+ */
28
+ export declare function searchStandards(standards: Standard[], query: string, filters?: {
29
+ domain?: string;
30
+ status?: string;
31
+ category?: string;
32
+ }): Standard[];
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import matter from "gray-matter";
4
+ /**
5
+ * Recursively find all .md files under a directory.
6
+ */
7
+ function findMarkdownFiles(dir) {
8
+ const results = [];
9
+ if (!fs.existsSync(dir))
10
+ return results;
11
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
12
+ for (const entry of entries) {
13
+ const fullPath = path.join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ results.push(...findMarkdownFiles(fullPath));
16
+ }
17
+ else if (entry.name.endsWith(".md") && !entry.name.startsWith("_")) {
18
+ results.push(fullPath);
19
+ }
20
+ }
21
+ return results;
22
+ }
23
+ /**
24
+ * Infer the domain from the file path.
25
+ * e.g., docs/integration/standards/INTG-STD-001.md → "integration"
26
+ */
27
+ function inferDomain(filePath, docsRoot) {
28
+ const relative = path.relative(docsRoot, filePath);
29
+ const parts = relative.split(path.sep);
30
+ return parts[0] ?? "unknown";
31
+ }
32
+ /**
33
+ * Load and parse all standards documents from the docs directory.
34
+ */
35
+ export function loadStandards(docsRoot) {
36
+ const standards = [];
37
+ const files = findMarkdownFiles(docsRoot);
38
+ for (const filePath of files) {
39
+ try {
40
+ const raw = fs.readFileSync(filePath, "utf-8");
41
+ const { data, content } = matter(raw);
42
+ // Only include files with an identifier (i.e., actual standards/guidelines)
43
+ if (!data.identifier)
44
+ continue;
45
+ standards.push({
46
+ metadata: data,
47
+ content,
48
+ filePath,
49
+ domain: inferDomain(filePath, docsRoot),
50
+ });
51
+ }
52
+ catch (err) {
53
+ console.error(`Skipping unparsable file: ${filePath}`, err);
54
+ }
55
+ }
56
+ return standards;
57
+ }
58
+ /**
59
+ * Simple full-text search across standards.
60
+ */
61
+ export function searchStandards(standards, query, filters) {
62
+ const queryLower = query.toLowerCase();
63
+ return standards.filter((std) => {
64
+ // Apply filters first
65
+ if (filters?.domain && std.domain !== filters.domain)
66
+ return false;
67
+ if (filters?.status && std.metadata.status !== filters.status)
68
+ return false;
69
+ if (filters?.category && std.metadata.category !== filters.category)
70
+ return false;
71
+ // Full-text search across identifier, name, content, and machine_summary
72
+ const searchable = [
73
+ std.metadata.identifier,
74
+ std.metadata.name,
75
+ std.metadata.machine_summary ?? "",
76
+ std.content,
77
+ ]
78
+ .join(" ")
79
+ .toLowerCase();
80
+ return searchable.includes(queryLower);
81
+ });
82
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@precepts/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server exposing Precepts standards as tools and resources for AI agents",
5
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "main": "dist/index.js",
14
+ "bin": {
15
+ "precepts-mcp": "dist/index.js"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.29.0",
19
+ "@precepts/standards": "^0.3.1",
20
+ "gray-matter": "^4.0.3",
21
+ "zod": "^4.4.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "tsx": "^4.19.0",
26
+ "typescript": "~5.6.2"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "start": "node dist/index.js",
34
+ "dev": "tsx src/index.ts"
35
+ }
36
+ }