laconic-skill 0.1.0 → 0.1.1
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/MCP.md +47 -0
- package/bin/laconic-skill-mcp.mjs +99 -0
- package/bin/laconic-skill.mjs +5 -92
- package/lib/core.mjs +95 -0
- package/package.json +8 -2
package/MCP.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# laconic-skill MCP
|
|
2
|
+
|
|
3
|
+
Use `laconic-skill` from MCP-compatible agents.
|
|
4
|
+
|
|
5
|
+
## Claude Desktop
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"mcpServers": {
|
|
10
|
+
"laconic-skill": {
|
|
11
|
+
"command": "npx",
|
|
12
|
+
"args": ["-y", "laconic-skill", "laconic-skill-mcp"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
### laconic_check
|
|
21
|
+
|
|
22
|
+
Input:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"text": "Short direct answer.",
|
|
27
|
+
"receipt": true
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Output:
|
|
32
|
+
|
|
33
|
+
- check result
|
|
34
|
+
- metrics
|
|
35
|
+
- optional tamper-evident CLI receipt
|
|
36
|
+
|
|
37
|
+
## Not Shipped In MCP v0
|
|
38
|
+
|
|
39
|
+
`laconic_rewrite`, `laconic_pipeline`, and `laconic_memory_search` are intentionally not exposed yet. This npm package currently ships real check/receipt behavior only.
|
|
40
|
+
|
|
41
|
+
## Smoke Test
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx laconic-skill-mcp smoke
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The smoke test starts the MCP server over stdio, lists tools, calls `laconic_check`, and prints the returned check result JSON.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { VERSION, checkText } from "../lib/core.mjs";
|
|
10
|
+
|
|
11
|
+
function createServer() {
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "laconic-skill",
|
|
14
|
+
version: VERSION,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
server.registerTool(
|
|
18
|
+
"laconic_check",
|
|
19
|
+
{
|
|
20
|
+
title: "Laconic Check",
|
|
21
|
+
description: "Check answer text for Laconic output rules and optionally return a tamper-evident receipt.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
text: z.string().describe("Answer text to check."),
|
|
24
|
+
receipt: z.boolean().optional().describe("Include a tamper-evident CLI receipt."),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
async ({ text, receipt = false }) => {
|
|
28
|
+
const result = checkText({ text, file: "mcp-input", receipt });
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: JSON.stringify(result, null, 2),
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
structuredContent: result,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return server;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runServer() {
|
|
45
|
+
const server = createServer();
|
|
46
|
+
const transport = new StdioServerTransport();
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runSmoke() {
|
|
51
|
+
const transport = new StdioClientTransport({
|
|
52
|
+
command: process.execPath,
|
|
53
|
+
args: [fileURLToPath(import.meta.url)],
|
|
54
|
+
stderr: "pipe",
|
|
55
|
+
});
|
|
56
|
+
const client = new Client({
|
|
57
|
+
name: "laconic-skill-smoke",
|
|
58
|
+
version: VERSION,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await client.connect(transport);
|
|
62
|
+
try {
|
|
63
|
+
const tools = await client.listTools();
|
|
64
|
+
const checkResult = await client.callTool({
|
|
65
|
+
name: "laconic_check",
|
|
66
|
+
arguments: {
|
|
67
|
+
text: "Short direct answer.",
|
|
68
|
+
receipt: true,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
JSON.stringify(
|
|
74
|
+
{
|
|
75
|
+
ok: true,
|
|
76
|
+
tools: tools.tools.map((tool) => tool.name),
|
|
77
|
+
laconic_check: JSON.parse(checkResult.content[0].text),
|
|
78
|
+
},
|
|
79
|
+
null,
|
|
80
|
+
2,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
await client.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const command = process.argv[2];
|
|
89
|
+
if (command === "smoke") {
|
|
90
|
+
runSmoke().catch((error) => {
|
|
91
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
runServer().catch((error) => {
|
|
96
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
});
|
|
99
|
+
}
|
package/bin/laconic-skill.mjs
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const VERSION = "0.1.0";
|
|
8
|
-
const FILLER_PREFIXES = [
|
|
9
|
-
"sure",
|
|
10
|
-
"certainly",
|
|
11
|
-
"of course",
|
|
12
|
-
"absolutely",
|
|
13
|
-
"as an ai",
|
|
14
|
-
"i'd be happy to",
|
|
15
|
-
"i can help",
|
|
16
|
-
];
|
|
4
|
+
import { VERSION, checkText } from "../lib/core.mjs";
|
|
17
5
|
|
|
18
6
|
function printHelp() {
|
|
19
7
|
console.log(`laconic-skill ${VERSION}
|
|
@@ -35,67 +23,6 @@ Options:
|
|
|
35
23
|
`);
|
|
36
24
|
}
|
|
37
25
|
|
|
38
|
-
function sha256(text) {
|
|
39
|
-
return createHash("sha256").update(text).digest("hex");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function stableJson(value) {
|
|
43
|
-
if (Array.isArray(value)) {
|
|
44
|
-
return `[${value.map(stableJson).join(",")}]`;
|
|
45
|
-
}
|
|
46
|
-
if (value && typeof value === "object") {
|
|
47
|
-
return `{${Object.keys(value)
|
|
48
|
-
.sort()
|
|
49
|
-
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
50
|
-
.join(",")}}`;
|
|
51
|
-
}
|
|
52
|
-
return JSON.stringify(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function countWords(text) {
|
|
56
|
-
const matches = text.trim().match(/\S+/g);
|
|
57
|
-
return matches ? matches.length : 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function readingTimeSeconds(text) {
|
|
61
|
-
return Math.max(1, Math.ceil((countWords(text) / 225) * 60));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function runChecks(answer) {
|
|
65
|
-
const trimmed = answer.trim();
|
|
66
|
-
const normalized = trimmed.toLowerCase();
|
|
67
|
-
const hasMarkdownBold = /\*\*[^*]+\*\*|__[^_]+__/.test(answer);
|
|
68
|
-
const direct = normalized.length > 0 && !FILLER_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
69
|
-
const complete = normalized.length > 0 && !normalized.endsWith("...");
|
|
70
|
-
const brief = countWords(answer) <= 120;
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
direct,
|
|
74
|
-
complete,
|
|
75
|
-
brief,
|
|
76
|
-
no_markdown_bold: !hasMarkdownBold,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function buildReceipt({ file, answer, checks }) {
|
|
81
|
-
const timestamp = new Date().toISOString();
|
|
82
|
-
const canonical = {
|
|
83
|
-
tool: "laconic-skill",
|
|
84
|
-
version: VERSION,
|
|
85
|
-
file: basename(file),
|
|
86
|
-
answer_hash: sha256(answer),
|
|
87
|
-
answer_length: countWords(answer),
|
|
88
|
-
reading_time_seconds: readingTimeSeconds(answer),
|
|
89
|
-
checks,
|
|
90
|
-
timestamp,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
...canonical,
|
|
95
|
-
receipt_hash: sha256(stableJson(canonical)),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
26
|
async function readStdin() {
|
|
100
27
|
const chunks = [];
|
|
101
28
|
for await (const chunk of process.stdin) {
|
|
@@ -118,32 +45,18 @@ async function checkCommand(args) {
|
|
|
118
45
|
}
|
|
119
46
|
|
|
120
47
|
const answer = file === "-" ? await readStdin() : await readFile(file, "utf8");
|
|
121
|
-
const
|
|
122
|
-
const passed = Object.values(checks).every(Boolean);
|
|
123
|
-
const result = {
|
|
124
|
-
ok: passed,
|
|
125
|
-
file,
|
|
126
|
-
checks,
|
|
127
|
-
metrics: {
|
|
128
|
-
answer_length: countWords(answer),
|
|
129
|
-
reading_time_seconds: readingTimeSeconds(answer),
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
if (includeReceipt) {
|
|
134
|
-
result.receipt = buildReceipt({ file, answer, checks });
|
|
135
|
-
}
|
|
48
|
+
const result = checkText({ text: answer, file, receipt: includeReceipt });
|
|
136
49
|
|
|
137
50
|
if (jsonOnly || includeReceipt) {
|
|
138
51
|
console.log(JSON.stringify(result, null, 2));
|
|
139
52
|
} else {
|
|
140
|
-
console.log(
|
|
141
|
-
for (const [name, value] of Object.entries(checks)) {
|
|
53
|
+
console.log(result.ok ? "OK" : "FAILED");
|
|
54
|
+
for (const [name, value] of Object.entries(result.checks)) {
|
|
142
55
|
console.log(`${name}: ${value}`);
|
|
143
56
|
}
|
|
144
57
|
}
|
|
145
58
|
|
|
146
|
-
process.exitCode =
|
|
59
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
147
60
|
}
|
|
148
61
|
|
|
149
62
|
async function main() {
|
package/lib/core.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const VERSION = "0.1.1";
|
|
5
|
+
|
|
6
|
+
const FILLER_PREFIXES = [
|
|
7
|
+
"sure",
|
|
8
|
+
"certainly",
|
|
9
|
+
"of course",
|
|
10
|
+
"absolutely",
|
|
11
|
+
"as an ai",
|
|
12
|
+
"i'd be happy to",
|
|
13
|
+
"i can help",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function sha256(text) {
|
|
17
|
+
return createHash("sha256").update(text).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stableJson(value) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
23
|
+
}
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
return `{${Object.keys(value)
|
|
26
|
+
.sort()
|
|
27
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
28
|
+
.join(",")}}`;
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function countWords(text) {
|
|
34
|
+
const matches = text.trim().match(/\S+/g);
|
|
35
|
+
return matches ? matches.length : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readingTimeSeconds(text) {
|
|
39
|
+
return Math.max(1, Math.ceil((countWords(text) / 225) * 60));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runChecks(answer) {
|
|
43
|
+
const trimmed = answer.trim();
|
|
44
|
+
const normalized = trimmed.toLowerCase();
|
|
45
|
+
const hasMarkdownBold = /\*\*[^*]+\*\*|__[^_]+__/.test(answer);
|
|
46
|
+
const direct = normalized.length > 0 && !FILLER_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
47
|
+
const complete = normalized.length > 0 && !normalized.endsWith("...");
|
|
48
|
+
const brief = countWords(answer) <= 120;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
direct,
|
|
52
|
+
complete,
|
|
53
|
+
brief,
|
|
54
|
+
no_markdown_bold: !hasMarkdownBold,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildReceipt({ file, answer, checks }) {
|
|
59
|
+
const timestamp = new Date().toISOString();
|
|
60
|
+
const canonical = {
|
|
61
|
+
tool: "laconic-skill",
|
|
62
|
+
version: VERSION,
|
|
63
|
+
file: file === "-" ? "-" : basename(file),
|
|
64
|
+
answer_hash: sha256(answer),
|
|
65
|
+
answer_length: countWords(answer),
|
|
66
|
+
reading_time_seconds: readingTimeSeconds(answer),
|
|
67
|
+
checks,
|
|
68
|
+
timestamp,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...canonical,
|
|
73
|
+
receipt_hash: sha256(stableJson(canonical)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function checkText({ text, file = "text", receipt = false }) {
|
|
78
|
+
const checks = runChecks(text);
|
|
79
|
+
const passed = Object.values(checks).every(Boolean);
|
|
80
|
+
const result = {
|
|
81
|
+
ok: passed,
|
|
82
|
+
file,
|
|
83
|
+
checks,
|
|
84
|
+
metrics: {
|
|
85
|
+
answer_length: countWords(text),
|
|
86
|
+
reading_time_seconds: readingTimeSeconds(text),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (receipt) {
|
|
91
|
+
result.receipt = buildReceipt({ file, answer: text, checks });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "laconic-skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A command-line checker for Laconic answers and tamper-evident receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"laconic-skill": "bin/laconic-skill.mjs",
|
|
11
|
-
"laconic": "bin/laconic-skill.mjs"
|
|
11
|
+
"laconic": "bin/laconic-skill.mjs",
|
|
12
|
+
"laconic-skill-mcp": "bin/laconic-skill-mcp.mjs"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin",
|
|
16
|
+
"lib",
|
|
17
|
+
"MCP.md",
|
|
15
18
|
"README.md"
|
|
16
19
|
],
|
|
17
20
|
"keywords": [
|
|
@@ -32,5 +35,8 @@
|
|
|
32
35
|
"license": "MIT",
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
35
41
|
}
|
|
36
42
|
}
|