@loomweaver/mcp 0.8.3 → 0.9.0-preview.10

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.
Files changed (2) hide show
  1. package/dist/main.mjs +320 -43
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -2984,7 +2984,7 @@ var require_compile = __commonJS({
2984
2984
  const schOrFunc = root.refs[ref];
2985
2985
  if (schOrFunc)
2986
2986
  return schOrFunc;
2987
- let _sch = resolve.call(this, root, ref);
2987
+ let _sch = resolve2.call(this, root, ref);
2988
2988
  if (_sch === void 0) {
2989
2989
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2990
2990
  const { schemaId } = this.opts;
@@ -3011,7 +3011,7 @@ var require_compile = __commonJS({
3011
3011
  function sameSchemaEnv(s1, s2) {
3012
3012
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3013
3013
  }
3014
- function resolve(root, ref) {
3014
+ function resolve2(root, ref) {
3015
3015
  let sch;
3016
3016
  while (typeof (sch = this.refs[ref]) == "string")
3017
3017
  ref = sch;
@@ -3642,7 +3642,7 @@ var require_fast_uri = __commonJS({
3642
3642
  }
3643
3643
  return uri;
3644
3644
  }
3645
- function resolve(baseURI, relativeURI, options) {
3645
+ function resolve2(baseURI, relativeURI, options) {
3646
3646
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3647
3647
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3648
3648
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3926,7 +3926,7 @@ var require_fast_uri = __commonJS({
3926
3926
  var fastUri = {
3927
3927
  SCHEMES,
3928
3928
  normalize,
3929
- resolve,
3929
+ resolve: resolve2,
3930
3930
  resolveComponent,
3931
3931
  equal,
3932
3932
  serialize,
@@ -23056,12 +23056,12 @@ var StdioServerTransport = class {
23056
23056
  this.onclose?.();
23057
23057
  }
23058
23058
  send(message) {
23059
- return new Promise((resolve) => {
23059
+ return new Promise((resolve2) => {
23060
23060
  const json2 = serializeMessage(message);
23061
23061
  if (this._stdout.write(json2)) {
23062
- resolve();
23062
+ resolve2();
23063
23063
  } else {
23064
- this._stdout.once("drain", resolve);
23064
+ this._stdout.once("drain", resolve2);
23065
23065
  }
23066
23066
  });
23067
23067
  }
@@ -28980,7 +28980,7 @@ var Protocol = class {
28980
28980
  return;
28981
28981
  }
28982
28982
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
28983
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
28983
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
28984
28984
  options?.signal?.throwIfAborted();
28985
28985
  }
28986
28986
  } catch (error51) {
@@ -28997,7 +28997,7 @@ var Protocol = class {
28997
28997
  */
28998
28998
  request(request, resultSchema, options) {
28999
28999
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
29000
- return new Promise((resolve, reject) => {
29000
+ return new Promise((resolve2, reject) => {
29001
29001
  const earlyReject = (error51) => {
29002
29002
  reject(error51);
29003
29003
  };
@@ -29075,7 +29075,7 @@ var Protocol = class {
29075
29075
  if (!parseResult.success) {
29076
29076
  reject(parseResult.error);
29077
29077
  } else {
29078
- resolve(parseResult.data);
29078
+ resolve2(parseResult.data);
29079
29079
  }
29080
29080
  } catch (error51) {
29081
29081
  reject(error51);
@@ -29336,12 +29336,12 @@ var Protocol = class {
29336
29336
  }
29337
29337
  } catch {
29338
29338
  }
29339
- return new Promise((resolve, reject) => {
29339
+ return new Promise((resolve2, reject) => {
29340
29340
  if (signal.aborted) {
29341
29341
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
29342
29342
  return;
29343
29343
  }
29344
- const timeoutId = setTimeout(resolve, interval);
29344
+ const timeoutId = setTimeout(resolve2, interval);
29345
29345
  signal.addEventListener("abort", () => {
29346
29346
  clearTimeout(timeoutId);
29347
29347
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -30432,7 +30432,7 @@ var McpServer = class {
30432
30432
  let task = createTaskResult.task;
30433
30433
  const pollInterval = task.pollInterval ?? 5e3;
30434
30434
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
30435
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
30435
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
30436
30436
  const updatedTask = await extra.taskStore.getTask(taskId);
30437
30437
  if (!updatedTask) {
30438
30438
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -31175,6 +31175,12 @@ export class ${w.className}AgentPanel {
31175
31175
  private runs = 0;
31176
31176
 
31177
31177
  constructor() {
31178
+ this.refreshOffered();
31179
+ }
31180
+
31181
+ // The list is read again whenever the panel gains focus, because the weaver's own translations
31182
+ // and other plugins' commands may arrive after this component was built.
31183
+ protected refreshOffered(): void {
31178
31184
  this.offered.set(${w.propertyName}Agent()?.list() ?? []);
31179
31185
  }
31180
31186
 
@@ -31265,7 +31271,7 @@ export class ${w.className}AgentPanel {
31265
31271
  `;
31266
31272
  }
31267
31273
  function panelTemplateFile(w) {
31268
- return `<div class="flex h-full flex-col gap-3">
31274
+ return `<div class="flex h-full flex-col gap-3" (focusin)="refreshOffered()">
31269
31275
  <p
31270
31276
  class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
31271
31277
  >
@@ -31381,10 +31387,10 @@ export async function* askAgent(
31381
31387
  toolCallName: picked.name,
31382
31388
  });
31383
31389
  // Arguments arrive in pieces, exactly as they do from a real model. The adapter assembles them;
31384
- // nothing downstream ever sees a half-written call. A real agent fills them from the JSON Schema
31385
- // the workbench described in picked.parameters; a stand-in has nothing to fill them from, so it
31386
- // sends none and lets the command apply its own defaults.
31387
- for (const piece of chunks(JSON.stringify({}), 4)) {
31390
+ // nothing downstream ever sees a half-written call. The values come from the JSON Schema the
31391
+ // workbench described in picked.parameters: the first declared choice of the first argument that
31392
+ // has choices, and nothing for the rest, so the command applies its own defaults there.
31393
+ for (const piece of chunks(JSON.stringify(sampleArguments(picked)), 4)) {
31388
31394
  yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
31389
31395
  await pause();
31390
31396
  }
@@ -31409,6 +31415,18 @@ async function* speak(
31409
31415
  yield event(EventType.TEXT_MESSAGE_END, { messageId });
31410
31416
  }
31411
31417
 
31418
+ function sampleArguments(tool: Tool): Record<string, unknown> {
31419
+ const schema = tool.parameters as
31420
+ | { properties?: Record<string, { enum?: readonly unknown[] }> }
31421
+ | undefined;
31422
+ for (const [name, property] of Object.entries(schema?.properties ?? {})) {
31423
+ if (property.enum && property.enum.length > 0) {
31424
+ return { [name]: property.enum[0] };
31425
+ }
31426
+ }
31427
+ return {};
31428
+ }
31429
+
31412
31430
  function chunks(text: string, size: number): readonly string[] {
31413
31431
  const pieces: string[] = [];
31414
31432
  for (let at = 0; at < text.length; at += size) {
@@ -31428,7 +31446,7 @@ function pause(): Promise<void> {
31428
31446
  }
31429
31447
 
31430
31448
  // ../devkit/src/recipes/angular-weaver/agent-files.ts
31431
- var AG_UI_ADAPTER_VERSION = "0.8.3";
31449
+ var AG_UI_ADAPTER_VERSION = "0.9.0-preview.10";
31432
31450
  var AG_UI_PROTOCOL_VERSION = "0.0.x";
31433
31451
  function connectionFile(w) {
31434
31452
  return `import { signal } from '@angular/core';
@@ -31494,7 +31512,7 @@ function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
31494
31512
  ],
31495
31513
  invokeCommand: (id: string, args?: CommandArguments) => {
31496
31514
  ran.push({ id, args });
31497
- return Promise.resolve({ outcome: 'answered', value: 'it ran' });
31515
+ return Promise.resolve({ outcome: 'answered', value: { tone: args?.['tone'] ?? 'info' } });
31498
31516
  },
31499
31517
  ui: { confirm: () => Promise.resolve(confirms) },
31500
31518
  } as unknown as PluginContext;
@@ -31523,17 +31541,17 @@ describe('${w.propertyName}Connection', () => {
31523
31541
  ),
31524
31542
  ).toBeNull();
31525
31543
  await tools.receive(
31526
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"who"' }),
31544
+ event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"tone"' }),
31527
31545
  );
31528
31546
  await tools.receive(
31529
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"you"}' }),
31547
+ event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"success"}' }),
31530
31548
  );
31531
31549
  const answer = await tools.receive(
31532
31550
  event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
31533
31551
  );
31534
31552
 
31535
- expect(ran).toEqual([{ id: '${w.id}.hello', args: { who: 'you' } }]);
31536
- expect(answer?.content).toBe('it ran');
31553
+ expect(ran).toEqual([{ id: '${w.id}.hello', args: { tone: 'success' } }]);
31554
+ expect(JSON.parse(answer?.content ?? '{}').tone).toBe('success');
31537
31555
  expect(answer?.error).toBeUndefined();
31538
31556
  });
31539
31557
 
@@ -31567,6 +31585,7 @@ function agentSurfaceBlock(w) {
31567
31585
  ` title: '${w.id}.agent.title',`,
31568
31586
  ` icon: '${w.id}',`,
31569
31587
  " docks: ['right-panel'],",
31588
+ " padded: true,",
31570
31589
  ` component: ${w.className}AgentPanel,`,
31571
31590
  " });"
31572
31591
  ].join("\n");
@@ -31738,7 +31757,9 @@ function i18nBundle(w) {
31738
31757
  }
31739
31758
  if (w.features.command) {
31740
31759
  bundle["action"] = `${w.name} action`;
31741
- bundle["actionDescription"] = `Shows a short ${w.name} message.`;
31760
+ bundle["actionDescription"] = `Shows a short ${w.name} message in the tone the caller chooses.`;
31761
+ bundle["actionTone"] = "How the message is shown: info, success or warning. Use info unless the caller asked for emphasis.";
31762
+ bundle["actionAnswers"] = "The tone the message was shown in.";
31742
31763
  }
31743
31764
  if (w.features.about) bundle["about"] = `About ${w.name}`;
31744
31765
  if (w.features.agent) {
@@ -31760,6 +31781,38 @@ function i18nFile(w) {
31760
31781
  return JSON.stringify(i18nBundle(w), null, 2) + "\n";
31761
31782
  }
31762
31783
 
31784
+ // ../devkit/src/recipes/angular-weaver/weaver-command.ts
31785
+ function commandBlock(w) {
31786
+ return [
31787
+ " ctx.registerCommand({",
31788
+ ` id: '${w.id}.hello',`,
31789
+ ` title: '${w.id}.action',`,
31790
+ ` description: '${w.id}.actionDescription',`,
31791
+ " arguments: [",
31792
+ ` { name: 'tone', kind: 'choice', choices: ['info', 'success', 'warning'], description: '${w.id}.actionTone' },`,
31793
+ " ],",
31794
+ ` answers: '${w.id}.actionAnswers',`,
31795
+ ` shortcut: '${w.features.shortcut}',`,
31796
+ " callable: true,",
31797
+ " run: (_context, args) => {",
31798
+ " const tone = toneOf(args?.['tone']);",
31799
+ ` ctx.ui.toast({ message: '${w.id}.action', kind: tone });`,
31800
+ " return { tone };",
31801
+ " },",
31802
+ " });"
31803
+ ].join("\n");
31804
+ }
31805
+ function toneHelper() {
31806
+ return [
31807
+ "type Tone = 'info' | 'success' | 'warning';",
31808
+ "",
31809
+ "// The workbench already refused anything outside the declared choices; this narrows the type.",
31810
+ "function toneOf(value: unknown): Tone {",
31811
+ " return value === 'success' || value === 'warning' ? value : 'info';",
31812
+ "}"
31813
+ ].join("\n");
31814
+ }
31815
+
31763
31816
  // ../devkit/src/recipes/angular-weaver/recipe.ts
31764
31817
  var DEFAULT_MENU_SLOT = "content/tab/context";
31765
31818
  var SURFACE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>';
@@ -31938,18 +31991,6 @@ function railBlock(w) {
31938
31991
  lines.push(" });");
31939
31992
  return lines.join("\n");
31940
31993
  }
31941
- function commandBlock(w) {
31942
- return [
31943
- " ctx.registerCommand({",
31944
- ` id: '${w.id}.hello',`,
31945
- ` title: '${w.id}.action',`,
31946
- ` description: '${w.id}.actionDescription',`,
31947
- ` shortcut: '${w.features.shortcut}',`,
31948
- " callable: true,",
31949
- ` run: () => ctx.ui.toast({ message: '${w.id}.action', kind: 'info' }),`,
31950
- " });"
31951
- ].join("\n");
31952
- }
31953
31994
  function barItemBlock(w) {
31954
31995
  return [
31955
31996
  " ctx.registerBarItem({",
@@ -32061,9 +32102,10 @@ function pluginFile(w) {
32061
32102
  deactivate() {
32062
32103
  ${w.propertyName}Agent.set(null);
32063
32104
  },` : "";
32105
+ if (w.features.command) consts.push(toneHelper());
32064
32106
  return `${imports.join("\n")}
32065
32107
 
32066
- ${consts.join("\n")}
32108
+ ${consts.join("\n\n")}
32067
32109
 
32068
32110
  export const ${w.propertyName}Plugin: Plugin = {
32069
32111
  manifest: {
@@ -32502,9 +32544,9 @@ function readme2(d) {
32502
32544
  "project's `project.json`, keeping the targets, `implicitDependencies` and tags it already had.",
32503
32545
  "",
32504
32546
  "Two leftovers from `ng new` are no longer referenced and can go: `src/app/app.routes.ts` (the",
32505
- "shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The generated",
32506
- "`src/app/app.spec.ts` now fails \u2014 `App` pulls the whole shell into a bare `TestBed` \u2014 so delete",
32507
- "it and test your own components instead of the composition root.",
32547
+ "shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The starter test",
32548
+ "`src/app/app.spec.ts` was replaced by one that boots the shell with this composition root, so",
32549
+ "`ng test` is green as generated; test your own components beside it.",
32508
32550
  "",
32509
32551
  "The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
32510
32552
  "one would fail a lint policy you never opted this project into. If your workspace enforces",
@@ -32758,6 +32800,31 @@ describe('layout', () => {
32758
32800
  });
32759
32801
  `;
32760
32802
  }
32803
+ function appSpec() {
32804
+ return `import { TestBed } from '@angular/core/testing';
32805
+ import { App } from './app';
32806
+ import { appConfig } from './app.config';
32807
+
32808
+ /* The application's starter test, rewritten for a root component that renders the shell: it boots
32809
+ with the composition root's own providers, so whatever you compose in is what the test boots
32810
+ with, and it asserts only that the shell mounted. */
32811
+ describe('App', () => {
32812
+ beforeEach(async () => {
32813
+ await TestBed.configureTestingModule({
32814
+ imports: [App],
32815
+ providers: appConfig.providers,
32816
+ }).compileComponents();
32817
+ });
32818
+
32819
+ it('boots the shell', async () => {
32820
+ const fixture = TestBed.createComponent(App);
32821
+ await fixture.whenStable();
32822
+ const compiled = fixture.nativeElement as HTMLElement;
32823
+ expect(compiled.querySelector('lw-shell')).toBeTruthy();
32824
+ });
32825
+ });
32826
+ `;
32827
+ }
32761
32828
  function indexHtml(d) {
32762
32829
  return `<!DOCTYPE html>
32763
32830
  <html lang="en">
@@ -32884,7 +32951,10 @@ var angularDistribution = {
32884
32951
  return {
32885
32952
  "src/main.ts": mainTs(),
32886
32953
  "src/app/app.config.ts": appConfigTs(d),
32887
- ...d.withTests && { "src/app/app.config.spec.ts": appConfigSpec() },
32954
+ ...d.withTests && {
32955
+ "src/app/app.config.spec.ts": appConfigSpec(),
32956
+ "src/app/app.spec.ts": appSpec()
32957
+ },
32888
32958
  "src/app/app.ts": appTs(),
32889
32959
  "src/app/app.html": appHtml(),
32890
32960
  "src/index.html": indexHtml(d),
@@ -33124,6 +33194,8 @@ function bootstrapFile(t) {
33124
33194
  --lw-on-info: #000;
33125
33195
 
33126
33196
  --lw-scrim: rgb(0 0 0 / 0.5);
33197
+ --lw-scroll-thumb: rgb(0 0 0 / 0.3);
33198
+ --lw-scroll-track: transparent;
33127
33199
 
33128
33200
  --lw-font-sans: var(--bs-body-font-family);
33129
33201
  --lw-font-mono: var(--bs-font-monospace);
@@ -33797,6 +33869,184 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
33797
33869
  return findings;
33798
33870
  }
33799
33871
 
33872
+ // ../devkit/src/lib/validate/commands.ts
33873
+ var RUNTIME_NOTE = "Grants, access and the window decide what is finally offered at runtime; this check judged the registrations alone.";
33874
+ function validateCommands(sources, ts) {
33875
+ const findings = [];
33876
+ for (const source of sources) {
33877
+ const file2 = ts.createSourceFile(source.path, source.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
33878
+ for (const call of registerCommandCalls(ts, file2)) {
33879
+ findings.push(...judge(read(ts, file2, call)));
33880
+ }
33881
+ }
33882
+ findings.push({ level: "info", code: "commands.runtime", message: RUNTIME_NOTE });
33883
+ return findings;
33884
+ }
33885
+ function registerCommandCalls(ts, file2) {
33886
+ const calls = [];
33887
+ const visit = (node) => {
33888
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "registerCommand") {
33889
+ calls.push(node);
33890
+ }
33891
+ ts.forEachChild(node, visit);
33892
+ };
33893
+ visit(file2);
33894
+ return calls;
33895
+ }
33896
+ function read(ts, file2, call) {
33897
+ const line = file2.getLineAndCharacterOfPosition(call.getStart(file2)).line + 1;
33898
+ const at2 = `${file2.fileName}:${line}`;
33899
+ const literal2 = call.arguments[0];
33900
+ const empty = { at: at2, callable: false, described: false, answers: false, returnsValue: false, arguments: [] };
33901
+ if (!literal2 || !ts.isObjectLiteralExpression(literal2)) {
33902
+ return { ...empty, unreadable: "its argument is not an object literal" };
33903
+ }
33904
+ const properties = /* @__PURE__ */ new Map();
33905
+ for (const property of literal2.properties) {
33906
+ if (ts.isSpreadAssignment(property)) {
33907
+ return { ...empty, unreadable: "it spreads another value into the registration" };
33908
+ }
33909
+ if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
33910
+ properties.set(property.name.text, property.initializer);
33911
+ } else if (ts.isMethodDeclaration(property) && ts.isIdentifier(property.name)) {
33912
+ properties.set(property.name.text, property);
33913
+ }
33914
+ }
33915
+ const id = properties.get("id");
33916
+ if (!id || !ts.isStringLiteral(id)) {
33917
+ return { ...empty, unreadable: "its id is not a string literal" };
33918
+ }
33919
+ const callable = properties.get("callable");
33920
+ return {
33921
+ at: at2,
33922
+ id: id.text,
33923
+ callable: callable !== void 0 && callable.kind === ts.SyntaxKind.TrueKeyword,
33924
+ described: properties.has("description"),
33925
+ answers: properties.has("answers"),
33926
+ returnsValue: returnsValue(ts, properties.get("run")),
33927
+ arguments: readArguments(ts, properties.get("arguments"))
33928
+ };
33929
+ }
33930
+ function readArguments(ts, value) {
33931
+ if (!value || !ts.isArrayLiteralExpression(value)) {
33932
+ return [];
33933
+ }
33934
+ return value.elements.map((element, index) => {
33935
+ if (!ts.isObjectLiteralExpression(element)) {
33936
+ return { name: `#${index + 1}`, described: false };
33937
+ }
33938
+ let name = `#${index + 1}`;
33939
+ let described = false;
33940
+ for (const property of element.properties) {
33941
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
33942
+ continue;
33943
+ }
33944
+ if (property.name.text === "name" && ts.isStringLiteral(property.initializer)) {
33945
+ name = property.initializer.text;
33946
+ }
33947
+ if (property.name.text === "description") {
33948
+ described = true;
33949
+ }
33950
+ }
33951
+ return { name, described };
33952
+ });
33953
+ }
33954
+ function returnsValue(ts, run) {
33955
+ if (!run) {
33956
+ return false;
33957
+ }
33958
+ if (ts.isArrowFunction(run) && !ts.isBlock(run.body)) {
33959
+ return ts.isObjectLiteralExpression(run.body) || ts.isArrayLiteralExpression(run.body);
33960
+ }
33961
+ let found = false;
33962
+ const visit = (node) => {
33963
+ if (found || ts.isFunctionLike(node) && node !== run) {
33964
+ return;
33965
+ }
33966
+ if (ts.isReturnStatement(node) && node.expression && node.expression.kind !== ts.SyntaxKind.UndefinedKeyword) {
33967
+ found = true;
33968
+ return;
33969
+ }
33970
+ ts.forEachChild(node, visit);
33971
+ };
33972
+ visit(run);
33973
+ return found;
33974
+ }
33975
+ function judge(registration) {
33976
+ const { at: at2, id } = registration;
33977
+ if (registration.unreadable) {
33978
+ return [
33979
+ {
33980
+ level: "info",
33981
+ code: "command.unreadable",
33982
+ path: at2,
33983
+ message: `A registerCommand call at ${at2} could not be read: ${registration.unreadable}. What it offers an agent is unknown here.`
33984
+ }
33985
+ ];
33986
+ }
33987
+ if (!registration.callable) {
33988
+ return [
33989
+ {
33990
+ level: "info",
33991
+ code: "command.private",
33992
+ path: at2,
33993
+ message: `${id}: not offered to an agent, because it is not callable; an agent never sees it.`
33994
+ }
33995
+ ];
33996
+ }
33997
+ const findings = [];
33998
+ if (!registration.described) {
33999
+ findings.push({
34000
+ level: "warning",
34001
+ code: "command.description",
34002
+ path: at2,
34003
+ message: `${id}: offered to an agent without a description; the agent sees only the id and has to guess what it does.`
34004
+ });
34005
+ }
34006
+ for (const argument of registration.arguments) {
34007
+ if (!argument.described) {
34008
+ findings.push({
34009
+ level: "info",
34010
+ code: "command.argument",
34011
+ path: at2,
34012
+ message: `${id}: argument "${argument.name}" has no description; an agent sees a bare name and guesses its value.`
34013
+ });
34014
+ }
34015
+ }
34016
+ if (registration.returnsValue && !registration.answers) {
34017
+ findings.push({
34018
+ level: "info",
34019
+ code: "command.answers",
34020
+ path: at2,
34021
+ message: `${id}: run returns a value but declares no answers; an agent hears that the command ran, not what it answered.`
34022
+ });
34023
+ }
34024
+ if (findings.length === 0) {
34025
+ const count = registration.arguments.length;
34026
+ findings.push({
34027
+ level: "info",
34028
+ code: "command.offered",
34029
+ path: at2,
34030
+ message: `${id}: offered to an agent, with ${count} described argument${count === 1 ? "" : "s"}${registration.answers ? " and a declared answer" : ""}.`
34031
+ });
34032
+ }
34033
+ return findings;
34034
+ }
34035
+
34036
+ // ../devkit/src/lib/validate/typescript.ts
34037
+ import { createRequire } from "node:module";
34038
+ import { resolve } from "node:path";
34039
+ function loadTypeScript(fromDir) {
34040
+ for (const from of [resolve(fromDir, "package.json"), resolve(process.cwd(), "package.json")]) {
34041
+ try {
34042
+ return createRequire(from)("typescript");
34043
+ } catch {
34044
+ continue;
34045
+ }
34046
+ }
34047
+ return null;
34048
+ }
34049
+
33800
34050
  // src/lib/tools.ts
33801
34051
  function ok(data) {
33802
34052
  return {
@@ -33852,6 +34102,23 @@ function validateI18nTool(a) {
33852
34102
  const bundles = a["bundles"] ?? {};
33853
34103
  return ok({ findings: validateI18nParity(bundles) });
33854
34104
  }
34105
+ function validateCommandsTool(a) {
34106
+ const files = a["files"] ?? {};
34107
+ const ts = loadTypeScript(process.cwd());
34108
+ if (!ts) {
34109
+ return ok({
34110
+ findings: [
34111
+ {
34112
+ level: "error",
34113
+ code: "commands.typescript",
34114
+ message: "typescript is not installed where this server runs; the check reads sources with the TypeScript compiler API, so start the server inside the project."
34115
+ }
34116
+ ]
34117
+ });
34118
+ }
34119
+ const sources = Object.entries(files).map(([path, text]) => ({ path, text }));
34120
+ return ok({ findings: validateCommands(sources, ts), note: RUNTIME_NOTE });
34121
+ }
33855
34122
 
33856
34123
  // src/lib/server.ts
33857
34124
  var FILE_MAP_NOTE = 'Returns a file map (relative path -> content); write the files into your project. May also return "remaining": steps the workspace needs that this route cannot perform, each saying what it costs to skip. Carry them out, or the generated output builds and does not work.';
@@ -33873,7 +34140,7 @@ function inputSchema(descriptor) {
33873
34140
  function createMcpServer() {
33874
34141
  const server = new McpServer({
33875
34142
  name: "loomweaver-devkit",
33876
- version: "0.8.3"
34143
+ version: "0.9.0-preview.10"
33877
34144
  });
33878
34145
  server.registerTool(
33879
34146
  "list_generators",
@@ -33915,6 +34182,16 @@ function createMcpServer() {
33915
34182
  },
33916
34183
  async (args) => validateI18nTool(args)
33917
34184
  );
34185
+ server.registerTool(
34186
+ "validate_commands",
34187
+ {
34188
+ description: "Say, per command a plugin registers, whether an agent is offered it and what would leave the agent guessing. Input: files = { <path>: <TypeScript source> }. Only a callable command without a description is a warning; the rest is information.",
34189
+ inputSchema: {
34190
+ files: external_exports.record(external_exports.string(), external_exports.string())
34191
+ }
34192
+ },
34193
+ async (args) => validateCommandsTool(args)
34194
+ );
33918
34195
  server.registerTool(
33919
34196
  "validate_catalog",
33920
34197
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomweaver/mcp",
3
- "version": "0.8.3",
3
+ "version": "0.9.0-preview.10",
4
4
  "description": "LoomWeaver: open-source plugin platform for Angular workbenches. The MCP server: scaffolding and validation as tools, so an AI assistant in any product repo can build weavers without a checkout.",
5
5
  "keywords": [
6
6
  "loomweaver",