@aetherwing/fcp-core 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/event-log.d.ts +78 -0
- package/dist/event-log.d.ts.map +1 -0
- package/dist/event-log.js +184 -0
- package/dist/event-log.js.map +1 -0
- package/dist/formatter.d.ts +19 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +64 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/parsed-op.d.ts +32 -0
- package/dist/parsed-op.d.ts.map +1 -0
- package/dist/parsed-op.js +41 -0
- package/dist/parsed-op.js.map +1 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +140 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +40 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +142 -0
- package/dist/session.js.map +1 -0
- package/dist/tokenizer.d.ts +26 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +114 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/verb-registry.d.ts +41 -0
- package/dist/verb-registry.d.ts.map +1 -0
- package/dist/verb-registry.js +65 -0
- package/dist/verb-registry.js.map +1 -0
- package/package.json +30 -0
- package/src/event-log.ts +209 -0
- package/src/formatter.ts +70 -0
- package/src/index.ts +40 -0
- package/src/parsed-op.ts +64 -0
- package/src/server.ts +241 -0
- package/src/session.ts +163 -0
- package/src/tokenizer.ts +108 -0
- package/src/verb-registry.ts +84 -0
- package/tests/event-log.test.ts +177 -0
- package/tests/formatter.test.ts +61 -0
- package/tests/parsed-op.test.ts +95 -0
- package/tests/server.test.ts +94 -0
- package/tests/session.test.ts +210 -0
- package/tests/tokenizer.test.ts +144 -0
- package/tests/verb-registry.test.ts +76 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { EventLog } from "./event-log.js";
|
|
4
|
+
import { parseOp, isParseError } from "./parsed-op.js";
|
|
5
|
+
import { VerbRegistry } from "./verb-registry.js";
|
|
6
|
+
import { SessionDispatcher } from "./session.js";
|
|
7
|
+
import { formatResult, suggest } from "./formatter.js";
|
|
8
|
+
/**
|
|
9
|
+
* Create an MCP server wired up with FCP conventions.
|
|
10
|
+
*
|
|
11
|
+
* Registers 4 tools:
|
|
12
|
+
* {domain} — primary mutation tool (ops array)
|
|
13
|
+
* {domain}_query — read-only queries
|
|
14
|
+
* {domain}_session — lifecycle (new, open, save, checkpoint, undo, redo)
|
|
15
|
+
* {domain}_help — reference card
|
|
16
|
+
*/
|
|
17
|
+
export function createFcpServer(config) {
|
|
18
|
+
const { domain, adapter, verbs, referenceCard } = config;
|
|
19
|
+
// Build registry and reference card
|
|
20
|
+
const registry = new VerbRegistry();
|
|
21
|
+
registry.registerMany(verbs);
|
|
22
|
+
const refCard = registry.generateReferenceCard(referenceCard?.sections);
|
|
23
|
+
// Event log
|
|
24
|
+
const eventLog = new EventLog();
|
|
25
|
+
// Session dispatcher with hooks
|
|
26
|
+
const sessionHooks = {
|
|
27
|
+
onNew(params) {
|
|
28
|
+
return adapter.createEmpty(params["title"] ?? "Untitled", params);
|
|
29
|
+
},
|
|
30
|
+
async onOpen(path) {
|
|
31
|
+
const { readFile } = await import("node:fs/promises");
|
|
32
|
+
const data = await readFile(path);
|
|
33
|
+
const model = adapter.deserialize(data);
|
|
34
|
+
adapter.rebuildIndices(model);
|
|
35
|
+
return model;
|
|
36
|
+
},
|
|
37
|
+
async onSave(model, path) {
|
|
38
|
+
const { writeFile } = await import("node:fs/promises");
|
|
39
|
+
const data = adapter.serialize(model);
|
|
40
|
+
await writeFile(path, data);
|
|
41
|
+
},
|
|
42
|
+
onRebuildIndices(model) {
|
|
43
|
+
adapter.rebuildIndices(model);
|
|
44
|
+
},
|
|
45
|
+
getDigest(model) {
|
|
46
|
+
return adapter.getDigest(model);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const session = new SessionDispatcher(sessionHooks, eventLog, {
|
|
50
|
+
reverseEvent: (event, model) => adapter.reverseEvent(event, model),
|
|
51
|
+
replayEvent: (event, model) => adapter.replayEvent(event, model),
|
|
52
|
+
});
|
|
53
|
+
// MCP server
|
|
54
|
+
const server = new McpServer({
|
|
55
|
+
name: `fcp-${domain}`,
|
|
56
|
+
version: "0.1.0",
|
|
57
|
+
});
|
|
58
|
+
// ── Primary mutation tool ──────────────────────────────
|
|
59
|
+
server.tool(domain, `Execute ${domain} operations. Each op string follows the FCP verb DSL.\n\n${refCard}`, {
|
|
60
|
+
ops: z
|
|
61
|
+
.array(z.string())
|
|
62
|
+
.describe("Array of operation strings"),
|
|
63
|
+
}, async ({ ops }) => {
|
|
64
|
+
const model = session.model;
|
|
65
|
+
if (!model) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: "error: no model loaded. Use session 'new' or 'open' first." }],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const lines = [];
|
|
72
|
+
let hasErrors = false;
|
|
73
|
+
for (const opStr of ops) {
|
|
74
|
+
const parsed = parseOp(opStr);
|
|
75
|
+
if (isParseError(parsed)) {
|
|
76
|
+
lines.push(`ERROR: ${parsed.error}`);
|
|
77
|
+
hasErrors = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Check if verb is known
|
|
81
|
+
const spec = registry.lookup(parsed.verb);
|
|
82
|
+
if (!spec) {
|
|
83
|
+
const suggestion = suggest(parsed.verb, verbs.map((v) => v.verb));
|
|
84
|
+
const msg = `unknown verb "${parsed.verb}"`;
|
|
85
|
+
lines.push(suggestion ? `ERROR: ${msg}\n try: ${suggestion}` : `ERROR: ${msg}`);
|
|
86
|
+
hasErrors = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const result = await adapter.dispatchOp(parsed, model, eventLog);
|
|
90
|
+
lines.push(formatResult(result.success, result.message, result.prefix));
|
|
91
|
+
if (!result.success)
|
|
92
|
+
hasErrors = true;
|
|
93
|
+
}
|
|
94
|
+
// Append digest
|
|
95
|
+
lines.push(adapter.getDigest(model));
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
98
|
+
isError: hasErrors,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
// ── Query tool ─────────────────────────────────────────
|
|
102
|
+
server.tool(`${domain}_query`, `Query ${domain} state. Read-only.`, {
|
|
103
|
+
q: z.string().describe("Query string"),
|
|
104
|
+
}, async ({ q }) => {
|
|
105
|
+
const model = session.model;
|
|
106
|
+
if (!model) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: "error: no model loaded." }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const result = await adapter.dispatchQuery(q, model);
|
|
113
|
+
if (typeof result === "string") {
|
|
114
|
+
return { content: [{ type: "text", text: result }] };
|
|
115
|
+
}
|
|
116
|
+
const qr = result;
|
|
117
|
+
const content = [];
|
|
118
|
+
if (qr.image) {
|
|
119
|
+
content.push({ type: "image", data: qr.image.base64, mimeType: qr.image.mimeType });
|
|
120
|
+
}
|
|
121
|
+
content.push({ type: "text", text: qr.text });
|
|
122
|
+
return { content };
|
|
123
|
+
});
|
|
124
|
+
// ── Session tool ───────────────────────────────────────
|
|
125
|
+
server.tool(`${domain}_session`, `${domain} lifecycle: new, open, save, checkpoint, undo, redo.`, {
|
|
126
|
+
action: z.string().describe("Action: 'new \"Title\"', 'open ./file', 'save', 'save as:./out', 'checkpoint v1', 'undo', 'undo to:v1', 'redo'"),
|
|
127
|
+
}, async ({ action }) => {
|
|
128
|
+
const text = await session.dispatch(action);
|
|
129
|
+
const model = session.model;
|
|
130
|
+
const digest = model ? adapter.getDigest(model) : "";
|
|
131
|
+
const output = digest ? `${text}\n${digest}` : text;
|
|
132
|
+
return { content: [{ type: "text", text: output }] };
|
|
133
|
+
});
|
|
134
|
+
// ── Help tool ──────────────────────────────────────────
|
|
135
|
+
server.tool(`${domain}_help`, `Returns the ${domain} FCP reference card.`, {}, async () => {
|
|
136
|
+
return { content: [{ type: "text", text: refCard }] };
|
|
137
|
+
});
|
|
138
|
+
return server;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { EventLog } from "./event-log.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hooks that a domain must implement for session lifecycle operations.
|
|
4
|
+
*/
|
|
5
|
+
export interface SessionHooks<Model> {
|
|
6
|
+
/** Create a new empty model with the given title/params. */
|
|
7
|
+
onNew(params: Record<string, string>): Model;
|
|
8
|
+
/** Open a model from a file path. */
|
|
9
|
+
onOpen(path: string): Promise<Model>;
|
|
10
|
+
/** Save a model to a file path. */
|
|
11
|
+
onSave(model: Model, path: string): Promise<void>;
|
|
12
|
+
/** Rebuild any derived indices after undo/redo. */
|
|
13
|
+
onRebuildIndices(model: Model): void;
|
|
14
|
+
/** Return a compact digest string for drift detection. */
|
|
15
|
+
getDigest(model: Model): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Routes session-level actions (new, open, save, checkpoint, undo, redo)
|
|
19
|
+
* to the appropriate handler. Framework handles checkpoint/undo/redo;
|
|
20
|
+
* domain handles new/open/save via hooks.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SessionDispatcher<Model, Event> {
|
|
23
|
+
private _model;
|
|
24
|
+
private _filePath;
|
|
25
|
+
private hooks;
|
|
26
|
+
private eventLog;
|
|
27
|
+
private reverseEvent;
|
|
28
|
+
private replayEvent;
|
|
29
|
+
constructor(hooks: SessionHooks<Model>, eventLog: EventLog<Event>, options: {
|
|
30
|
+
reverseEvent: (event: Event, model: Model) => void;
|
|
31
|
+
replayEvent: (event: Event, model: Model) => void;
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Dispatch a session action string. Returns a result message.
|
|
35
|
+
*/
|
|
36
|
+
dispatch(action: string): Promise<string>;
|
|
37
|
+
get model(): Model | null;
|
|
38
|
+
get filePath(): string | null;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC,4DAA4D;IAC5D,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IAC7C,qCAAqC;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACrC,mCAAmC;IACnC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,mDAAmD;IACnD,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACrC,0DAA0D;IAC1D,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;CACjC;AAED;;;;GAIG;AACH,qBAAa,iBAAiB,CAAC,KAAK,EAAE,KAAK;IACzC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,YAAY,CAAuC;IAC3D,OAAO,CAAC,WAAW,CAAuC;gBAGxD,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAC1B,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,EACzB,OAAO,EAAE;QACP,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;QACnD,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;KACnD;IAQH;;OAEG;IACG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0G/C,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAExB;IAED,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;CACF"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { tokenize, isKeyValue, parseKeyValue } from "./tokenizer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Routes session-level actions (new, open, save, checkpoint, undo, redo)
|
|
4
|
+
* to the appropriate handler. Framework handles checkpoint/undo/redo;
|
|
5
|
+
* domain handles new/open/save via hooks.
|
|
6
|
+
*/
|
|
7
|
+
export class SessionDispatcher {
|
|
8
|
+
_model = null;
|
|
9
|
+
_filePath = null;
|
|
10
|
+
hooks;
|
|
11
|
+
eventLog;
|
|
12
|
+
reverseEvent;
|
|
13
|
+
replayEvent;
|
|
14
|
+
constructor(hooks, eventLog, options) {
|
|
15
|
+
this.hooks = hooks;
|
|
16
|
+
this.eventLog = eventLog;
|
|
17
|
+
this.reverseEvent = options.reverseEvent;
|
|
18
|
+
this.replayEvent = options.replayEvent;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Dispatch a session action string. Returns a result message.
|
|
22
|
+
*/
|
|
23
|
+
async dispatch(action) {
|
|
24
|
+
const tokens = tokenize(action);
|
|
25
|
+
if (tokens.length === 0)
|
|
26
|
+
return "empty action";
|
|
27
|
+
const cmd = tokens[0].toLowerCase();
|
|
28
|
+
switch (cmd) {
|
|
29
|
+
case "new": {
|
|
30
|
+
const params = {};
|
|
31
|
+
const positionals = [];
|
|
32
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
33
|
+
if (isKeyValue(tokens[i])) {
|
|
34
|
+
const { key, value } = parseKeyValue(tokens[i]);
|
|
35
|
+
params[key] = value;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
positionals.push(tokens[i]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (positionals.length > 0) {
|
|
42
|
+
params["title"] = positionals[0];
|
|
43
|
+
}
|
|
44
|
+
this._model = this.hooks.onNew(params);
|
|
45
|
+
this._filePath = null;
|
|
46
|
+
const title = params["title"] ?? "Untitled";
|
|
47
|
+
return `new "${title}" created`;
|
|
48
|
+
}
|
|
49
|
+
case "open": {
|
|
50
|
+
const path = tokens[1];
|
|
51
|
+
if (!path)
|
|
52
|
+
return "open requires a file path";
|
|
53
|
+
try {
|
|
54
|
+
this._model = await this.hooks.onOpen(path);
|
|
55
|
+
this._filePath = path;
|
|
56
|
+
return `opened "${path}"`;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
60
|
+
return `error: ${msg}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
case "save": {
|
|
64
|
+
if (!this._model)
|
|
65
|
+
return "error: no model to save";
|
|
66
|
+
let savePath = this._filePath;
|
|
67
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
68
|
+
if (isKeyValue(tokens[i])) {
|
|
69
|
+
const { key, value } = parseKeyValue(tokens[i]);
|
|
70
|
+
if (key === "as")
|
|
71
|
+
savePath = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!savePath)
|
|
75
|
+
return "error: no file path. Use save as:./file";
|
|
76
|
+
try {
|
|
77
|
+
await this.hooks.onSave(this._model, savePath);
|
|
78
|
+
this._filePath = savePath;
|
|
79
|
+
return `saved "${savePath}"`;
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
83
|
+
return `error: ${msg}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
case "checkpoint": {
|
|
87
|
+
const name = tokens[1];
|
|
88
|
+
if (!name)
|
|
89
|
+
return "checkpoint requires a name";
|
|
90
|
+
this.eventLog.checkpoint(name);
|
|
91
|
+
return `checkpoint "${name}" created`;
|
|
92
|
+
}
|
|
93
|
+
case "undo": {
|
|
94
|
+
if (!this._model)
|
|
95
|
+
return "nothing to undo";
|
|
96
|
+
// undo to:NAME or undo [count]
|
|
97
|
+
if (tokens.length >= 2 && tokens[1].startsWith("to:")) {
|
|
98
|
+
const name = tokens[1].slice(3);
|
|
99
|
+
if (!name)
|
|
100
|
+
return "undo to: requires a checkpoint name";
|
|
101
|
+
const events = this.eventLog.undoTo(name);
|
|
102
|
+
if (!events)
|
|
103
|
+
return `cannot undo to "${name}"`;
|
|
104
|
+
for (const ev of events) {
|
|
105
|
+
this.reverseEvent(ev, this._model);
|
|
106
|
+
}
|
|
107
|
+
this.hooks.onRebuildIndices(this._model);
|
|
108
|
+
return `undone ${events.length} event${events.length !== 1 ? "s" : ""} to checkpoint "${name}"`;
|
|
109
|
+
}
|
|
110
|
+
const events = this.eventLog.undo();
|
|
111
|
+
if (events.length === 0)
|
|
112
|
+
return "nothing to undo";
|
|
113
|
+
for (const ev of events) {
|
|
114
|
+
this.reverseEvent(ev, this._model);
|
|
115
|
+
}
|
|
116
|
+
this.hooks.onRebuildIndices(this._model);
|
|
117
|
+
return `undone ${events.length} event${events.length !== 1 ? "s" : ""}`;
|
|
118
|
+
}
|
|
119
|
+
case "redo": {
|
|
120
|
+
if (!this._model)
|
|
121
|
+
return "nothing to redo";
|
|
122
|
+
const events = this.eventLog.redo();
|
|
123
|
+
if (events.length === 0)
|
|
124
|
+
return "nothing to redo";
|
|
125
|
+
for (const ev of events) {
|
|
126
|
+
this.replayEvent(ev, this._model);
|
|
127
|
+
}
|
|
128
|
+
this.hooks.onRebuildIndices(this._model);
|
|
129
|
+
return `redone ${events.length} event${events.length !== 1 ? "s" : ""}`;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
return `unknown session action "${cmd}"`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
get model() {
|
|
136
|
+
return this._model;
|
|
137
|
+
}
|
|
138
|
+
get filePath() {
|
|
139
|
+
return this._filePath;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize an operation string by whitespace, respecting quoted strings.
|
|
3
|
+
* "add svc \"Auth Service\" theme:blue" -> ["add", "svc", "Auth Service", "theme:blue"]
|
|
4
|
+
*/
|
|
5
|
+
export declare function tokenize(input: string): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Check if a token is a key:value pair.
|
|
8
|
+
* Must contain ":" but not start with "@" (selectors) and not be an arrow.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isKeyValue(token: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a key:value token. The value may include colons (e.g., "style:orthogonal").
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseKeyValue(token: string): {
|
|
15
|
+
key: string;
|
|
16
|
+
value: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Check if a token is an arrow operator.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isArrow(token: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a token is a selector (@-prefixed).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isSelector(token: string): boolean;
|
|
26
|
+
//# sourceMappingURL=tokenizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmEhD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAM3E;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjD"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize an operation string by whitespace, respecting quoted strings.
|
|
3
|
+
* "add svc \"Auth Service\" theme:blue" -> ["add", "svc", "Auth Service", "theme:blue"]
|
|
4
|
+
*/
|
|
5
|
+
export function tokenize(input) {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
let i = 0;
|
|
8
|
+
const len = input.length;
|
|
9
|
+
while (i < len) {
|
|
10
|
+
// Skip whitespace
|
|
11
|
+
while (i < len && input[i] === " ")
|
|
12
|
+
i++;
|
|
13
|
+
if (i >= len)
|
|
14
|
+
break;
|
|
15
|
+
if (input[i] === '"') {
|
|
16
|
+
// Quoted string
|
|
17
|
+
i++; // skip opening quote
|
|
18
|
+
let token = "";
|
|
19
|
+
while (i < len && input[i] !== '"') {
|
|
20
|
+
if (input[i] === "\\" && i + 1 < len) {
|
|
21
|
+
const next = input[i + 1];
|
|
22
|
+
if (next === "n") {
|
|
23
|
+
token += "\n";
|
|
24
|
+
i += 2;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
i++;
|
|
28
|
+
token += input[i];
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
token += input[i];
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (i < len)
|
|
38
|
+
i++; // skip closing quote
|
|
39
|
+
tokens.push(token);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Unquoted token — handle embedded quotes (e.g., key:"value")
|
|
43
|
+
let token = "";
|
|
44
|
+
while (i < len && input[i] !== " ") {
|
|
45
|
+
if (input[i] === '"') {
|
|
46
|
+
// Embedded quoted value
|
|
47
|
+
i++; // skip opening quote
|
|
48
|
+
while (i < len && input[i] !== '"') {
|
|
49
|
+
if (input[i] === "\\" && i + 1 < len) {
|
|
50
|
+
const next = input[i + 1];
|
|
51
|
+
if (next === "n") {
|
|
52
|
+
token += "\n";
|
|
53
|
+
i += 2;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
i++;
|
|
57
|
+
token += input[i];
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
token += input[i];
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (i < len)
|
|
67
|
+
i++; // skip closing quote
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
token += input[i];
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Convert literal \n in unquoted tokens to actual newlines
|
|
75
|
+
tokens.push(token.replace(/\\n/g, "\n"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return tokens;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a token is a key:value pair.
|
|
82
|
+
* Must contain ":" but not start with "@" (selectors) and not be an arrow.
|
|
83
|
+
*/
|
|
84
|
+
export function isKeyValue(token) {
|
|
85
|
+
if (token.startsWith("@"))
|
|
86
|
+
return false;
|
|
87
|
+
if (isArrow(token))
|
|
88
|
+
return false;
|
|
89
|
+
const colonIdx = token.indexOf(":");
|
|
90
|
+
return colonIdx > 0 && colonIdx < token.length - 1;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Parse a key:value token. The value may include colons (e.g., "style:orthogonal").
|
|
94
|
+
*/
|
|
95
|
+
export function parseKeyValue(token) {
|
|
96
|
+
const colonIdx = token.indexOf(":");
|
|
97
|
+
return {
|
|
98
|
+
key: token.slice(0, colonIdx),
|
|
99
|
+
value: token.slice(colonIdx + 1),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if a token is an arrow operator.
|
|
104
|
+
*/
|
|
105
|
+
export function isArrow(token) {
|
|
106
|
+
return token === "->" || token === "<->" || token === "--";
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if a token is a selector (@-prefixed).
|
|
110
|
+
*/
|
|
111
|
+
export function isSelector(token) {
|
|
112
|
+
return token.startsWith("@");
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=tokenizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specification for a single verb in an FCP protocol.
|
|
3
|
+
*/
|
|
4
|
+
export interface VerbSpec {
|
|
5
|
+
verb: string;
|
|
6
|
+
syntax: string;
|
|
7
|
+
category: string;
|
|
8
|
+
params?: string[];
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Registry of verb specifications. Provides lookup by verb name and
|
|
13
|
+
* reference card generation grouped by category.
|
|
14
|
+
*/
|
|
15
|
+
export declare class VerbRegistry {
|
|
16
|
+
private specs;
|
|
17
|
+
private categories;
|
|
18
|
+
/**
|
|
19
|
+
* Register a single verb specification.
|
|
20
|
+
*/
|
|
21
|
+
register(spec: VerbSpec): void;
|
|
22
|
+
/**
|
|
23
|
+
* Register multiple verb specifications at once.
|
|
24
|
+
*/
|
|
25
|
+
registerMany(specs: VerbSpec[]): void;
|
|
26
|
+
/**
|
|
27
|
+
* Look up a verb specification by name.
|
|
28
|
+
*/
|
|
29
|
+
lookup(verb: string): VerbSpec | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a reference card string grouped by category.
|
|
32
|
+
* Optional `sections` adds extra static sections (e.g., domain-specific
|
|
33
|
+
* reference material) appended after the verb listing.
|
|
34
|
+
*/
|
|
35
|
+
generateReferenceCard(sections?: Record<string, string>): string;
|
|
36
|
+
/**
|
|
37
|
+
* All registered verb specifications.
|
|
38
|
+
*/
|
|
39
|
+
get verbs(): VerbSpec[];
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=verb-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verb-registry.d.ts","sourceRoot":"","sources":["../src/verb-registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,UAAU,CAAiC;IAEnD;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAO9B;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI;IAMrC;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI1C;;;;OAIG;IACH,qBAAqB,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM;IA2BhE;;OAEG;IACH,IAAI,KAAK,IAAI,QAAQ,EAAE,CAEtB;CACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of verb specifications. Provides lookup by verb name and
|
|
3
|
+
* reference card generation grouped by category.
|
|
4
|
+
*/
|
|
5
|
+
export class VerbRegistry {
|
|
6
|
+
specs = new Map();
|
|
7
|
+
categories = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Register a single verb specification.
|
|
10
|
+
*/
|
|
11
|
+
register(spec) {
|
|
12
|
+
this.specs.set(spec.verb, spec);
|
|
13
|
+
const list = this.categories.get(spec.category) ?? [];
|
|
14
|
+
list.push(spec);
|
|
15
|
+
this.categories.set(spec.category, list);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register multiple verb specifications at once.
|
|
19
|
+
*/
|
|
20
|
+
registerMany(specs) {
|
|
21
|
+
for (const spec of specs) {
|
|
22
|
+
this.register(spec);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Look up a verb specification by name.
|
|
27
|
+
*/
|
|
28
|
+
lookup(verb) {
|
|
29
|
+
return this.specs.get(verb);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate a reference card string grouped by category.
|
|
33
|
+
* Optional `sections` adds extra static sections (e.g., domain-specific
|
|
34
|
+
* reference material) appended after the verb listing.
|
|
35
|
+
*/
|
|
36
|
+
generateReferenceCard(sections) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
for (const [category, specs] of this.categories) {
|
|
39
|
+
lines.push(`${category.toUpperCase()}:`);
|
|
40
|
+
for (const spec of specs) {
|
|
41
|
+
lines.push(` ${spec.syntax}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push("");
|
|
44
|
+
}
|
|
45
|
+
if (sections) {
|
|
46
|
+
for (const [title, content] of Object.entries(sections)) {
|
|
47
|
+
lines.push(`${title.toUpperCase()}:`);
|
|
48
|
+
lines.push(content);
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Remove trailing empty line
|
|
53
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
54
|
+
lines.pop();
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* All registered verb specifications.
|
|
60
|
+
*/
|
|
61
|
+
get verbs() {
|
|
62
|
+
return [...this.specs.values()];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=verb-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verb-registry.js","sourceRoot":"","sources":["../src/verb-registry.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,MAAM,OAAO,YAAY;IACf,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IACpC,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEnD;;OAEG;IACH,QAAQ,CAAC,IAAc;QACrB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,KAAiB;QAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,IAAY;QACjB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,qBAAqB,CAAC,QAAiC;QACrD,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAChD,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;YACzC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACjC,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxD,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;gBACtC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACpB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAED,6BAA6B;QAC7B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;YAC1D,KAAK,CAAC,GAAG,EAAE,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,IAAI,KAAK;QACP,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aetherwing/fcp-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "File Context Protocol — reusable framework for building MCP servers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.15.2",
|
|
24
|
+
"typescript": "^5.8.3",
|
|
25
|
+
"vitest": "^3.1.1"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|