aiohttp-tiny-mcp 0.5.0__tar.gz → 0.5.2__tar.gz

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 (58) hide show
  1. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/PKG-INFO +1 -1
  2. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/pyproject.toml +1 -1
  3. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/pyproject.toml.orig +1 -1
  4. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/client/stdio.py +6 -2
  5. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/console/console.css +10 -1
  6. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/console/console.js +212 -26
  7. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/console/index.html +1 -1
  8. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/LICENSE +0 -0
  9. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/README.md +0 -0
  10. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/__init__.py +0 -0
  11. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/auth/__init__.py +0 -0
  12. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/auth/jwt.py +0 -0
  13. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/client/__init__.py +0 -0
  14. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/client/base.py +0 -0
  15. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/client/http.py +0 -0
  16. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/console/__init__.py +0 -0
  17. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/console/oauth-callback.html +0 -0
  18. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/extensions/__init__.py +0 -0
  19. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/extensions/skills.py +0 -0
  20. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/oauth/__init__.py +0 -0
  21. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/oauth/github.py +0 -0
  22. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/oauth/provider.py +0 -0
  23. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/oauth/server.py +0 -0
  24. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/oauth/tokens.py +0 -0
  25. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/__init__.py +0 -0
  26. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/adapter.py +0 -0
  27. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/core.py +0 -0
  28. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/models.py +0 -0
  29. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/schema.py +0 -0
  30. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/selection.py +0 -0
  31. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/v2024_11_05.py +0 -0
  32. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/v2025_03_26.py +0 -0
  33. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/v2025_06_18.py +0 -0
  34. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/v2025_11_25.py +0 -0
  35. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/protocol/v2026_07_28.py +0 -0
  36. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/py.typed +0 -0
  37. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/__init__.py +0 -0
  38. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/dispatcher.py +0 -0
  39. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/exchange.py +0 -0
  40. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/http.py +0 -0
  41. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/registry.py +0 -0
  42. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/request_state.py +0 -0
  43. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/specs.py +0 -0
  44. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/sse.py +0 -0
  45. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/stdio.py +0 -0
  46. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/subscriptions.py +0 -0
  47. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/server/tasks.py +0 -0
  48. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/__init__.py +0 -0
  49. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/hub.py +0 -0
  50. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/namespaces.py +0 -0
  51. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/postgres.py +0 -0
  52. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/redis.py +0 -0
  53. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/sessions.py +0 -0
  54. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/storage/sqlite.py +0 -0
  55. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/testing.py +0 -0
  56. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/transport/__init__.py +0 -0
  57. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/transport/metadata.py +0 -0
  58. {aiohttp_tiny_mcp-0.5.0 → aiohttp_tiny_mcp-0.5.2}/src/aiohttp_tiny_mcp/transport/sse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohttp-tiny-mcp
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Remote MCP server and client library for aiohttp
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -76,10 +76,14 @@ class StdioClient(BaseClient):
76
76
  adapter: Adapter,
77
77
  client_info: Implementation | None = None,
78
78
  on_ask: Elicitor | None = None,
79
+ limit: int = 1024 * 1024,
79
80
  ) -> AsyncIterator[StdioClient]:
80
- """Launch cmd with protocol pipes; inherit stderr so logging cannot corrupt stdout."""
81
+ """Launch cmd with protocol pipes and inherit stderr.
82
+
83
+ `limit` bounds response lines in bytes and defaults to 1 MiB.
84
+ """
81
85
  process = await asyncio.create_subprocess_exec(
82
- *cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE
86
+ *cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, limit=limit
83
87
  )
84
88
  assert process.stdin is not None
85
89
  assert process.stdout is not None
@@ -70,6 +70,10 @@ body {
70
70
  letter-spacing: -0.01em;
71
71
  }
72
72
 
73
+ .bar h1 > button { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; }
74
+ .bar h1 > button:disabled { opacity: 1; }
75
+ .bar h1 > button:hover:not(:disabled) { text-decoration: underline; }
76
+
73
77
  .field { display: flex; align-items: center; gap: 6px; }
74
78
  .field.grow { flex: 1 1 12em; min-width: 0; }
75
79
  .field.grow > input { flex: 1 1 auto; min-width: 8em; }
@@ -334,7 +338,12 @@ button.quiet { color: var(--ink-soft); }
334
338
 
335
339
  .argument { display: flex; flex-direction: column; gap: 3px; }
336
340
 
337
- .argument > label { font-size: 12px; font-weight: 500; }
341
+ .choice-group { display: flex; flex-wrap: wrap; gap: 6px 16px; border: 0; padding: 0; margin: 0; min-width: 0; }
342
+ .choice-group > label { display: flex; align-items: center; gap: 5px; }
343
+ .argument-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
344
+ .argument .null-toggle { display: flex; align-items: center; gap: 4px; color: var(--ink-soft); font-weight: 400; }
345
+ .argument > :disabled { opacity: 0.45; cursor: not-allowed; }
346
+ .argument > label, .argument-heading > label { font-size: 12px; font-weight: 500; }
338
347
  .argument .hint { font-size: 11.5px; color: var(--ink-faint); }
339
348
  .argument .required { color: var(--bad); margin-left: 3px; }
340
349
 
@@ -540,6 +540,7 @@ function encodeHeader(value) {
540
540
 
541
541
 
542
542
  const page = {
543
+ serverTitle: document.getElementById("server-title"),
543
544
  authOAuth: document.getElementById("auth-oauth"),
544
545
  authOAuthStatus: document.getElementById("auth-oauth-status"),
545
546
  authenticate: document.getElementById("authenticate"),
@@ -585,6 +586,7 @@ const page = {
585
586
 
586
587
  let client = null;
587
588
  let chosen = null;
589
+ let serverInstructions = "";
588
590
  let endpointUrl = null;
589
591
  //: Where the page was told the endpoint is. The field starts from it and may add a query.
590
592
  let configuredEndpoint = null;
@@ -886,22 +888,74 @@ function buildFields(schema, into, omit) {
886
888
  return fields;
887
889
  }
888
890
 
891
+ let fieldId = 0;
892
+
893
+ function fieldChoices(property) {
894
+ if (Array.isArray(property.enum)) {
895
+ return property.enum.map((value) => ({ value, title: String(value) }));
896
+ }
897
+ if (!Array.isArray(property.oneOf) || !property.oneOf.length) return null;
898
+ const choices = [];
899
+ for (const branch of property.oneOf) {
900
+ if (!branch || typeof branch !== "object") return null;
901
+ let value;
902
+ if (branch.type === "null") value = null;
903
+ else if (Object.hasOwn(branch, "const")) value = branch.const;
904
+ else if (Array.isArray(branch.enum) && branch.enum.length === 1) value = branch.enum[0];
905
+ else return null;
906
+ choices.push({ value, title: branch.title ?? String(value) });
907
+ }
908
+ return choices;
909
+ }
910
+
911
+ function allowsNull(property) {
912
+ if (property.default === null) return true;
913
+ if (property.type === "null" || [].concat(property.type || []).includes("null")) return true;
914
+ if (Object.hasOwn(property, "const") && property.const === null) return true;
915
+ if (Array.isArray(property.enum) && property.enum.includes(null)) return true;
916
+ return (property.anyOf || property.oneOf || []).some(allowsNull);
917
+ }
918
+
889
919
  function buildField(name, property, isRequired, into) {
890
- const kinds = [].concat(property.type || inferType(property));
891
- const kind = kinds.find((one) => one !== "null") || "string";
892
- const row = element("div", kind === "boolean" ? "argument flag" : "argument");
920
+ const nullable = allowsNull(property);
921
+ const branches = property.anyOf || property.oneOf || [];
922
+ const nonNull = branches.filter((branch) => branch.type !== "null" &&
923
+ !(Object.hasOwn(branch, "const") && branch.const === null));
924
+ const effective = nullable && nonNull.length === 1
925
+ ? { ...nonNull[0], ...property, enum: property.enum || nonNull[0].enum }
926
+ : property;
927
+ const kinds = [].concat(effective.type || inferType(effective));
928
+ const choices = fieldChoices(effective)?.filter(({ value }) => value !== null);
929
+ const baseKind = kinds.find((one) => one !== "null") || "string";
930
+ const itemChoices = baseKind === "array" && effective.items ? fieldChoices(effective.items) : null;
931
+ const kind = choices ? "choice" : itemChoices ? "choices" : baseKind;
932
+ const row = element("div", kind === "boolean" && !nullable ? "argument flag" : "argument");
893
933
  const label = element("label", null, property.title || name);
894
934
  if (isRequired) label.append(element("span", "required", "*"));
895
935
  let input;
896
936
 
897
- if (property.enum) {
937
+ if (kind === "choices") {
938
+ input = element("fieldset", "choice-group");
939
+ input.choiceInputs = itemChoices.map(({ value, title }) => {
940
+ const option = element("label");
941
+ const checkbox = element("input");
942
+ checkbox.type = "checkbox";
943
+ checkbox.value = JSON.stringify(value);
944
+ checkbox.checked = Array.isArray(property.default) &&
945
+ property.default.some((item) => JSON.stringify(item) === checkbox.value);
946
+ option.append(checkbox, element("span", null, title));
947
+ input.append(option);
948
+ return checkbox;
949
+ });
950
+ } else if (choices) {
898
951
  input = element("select");
899
952
  if (!isRequired) input.append(element("option", null, ""));
900
- property.enum.forEach((value) => {
901
- const option = element("option", null, String(value));
902
- option.value = String(value);
953
+ choices.forEach(({ value, title }) => {
954
+ const option = element("option", null, title);
955
+ option.value = JSON.stringify(value);
903
956
  input.append(option);
904
957
  });
958
+ if (property.default !== undefined && property.default !== null) input.value = JSON.stringify(property.default);
905
959
  } else if (kind === "boolean") {
906
960
  input = element("input");
907
961
  input.type = "checkbox";
@@ -909,24 +963,44 @@ function buildField(name, property, isRequired, into) {
909
963
  } else if (kind === "integer" || kind === "number") {
910
964
  input = element("input");
911
965
  input.type = "number";
912
- if (kind === "integer") input.step = "1";
913
- if (property.minimum !== undefined) input.min = property.minimum;
914
- if (property.maximum !== undefined) input.max = property.maximum;
915
- if (property.default !== undefined) input.value = property.default;
966
+ input.step = kind === "integer" ? "1" : "any";
967
+ if (effective.minimum !== undefined) input.min = effective.minimum;
968
+ if (effective.maximum !== undefined) input.max = effective.maximum;
969
+ if (property.default !== undefined && property.default !== null) input.value = property.default;
916
970
  } else if (kind === "object" || kind === "array") {
917
971
  input = element("textarea");
918
972
  input.placeholder = kind === "array" ? "[]" : "{}";
919
- if (property.default !== undefined) input.value = JSON.stringify(property.default, null, 2);
973
+ if (property.default !== undefined && property.default !== null) input.value = JSON.stringify(property.default, null, 2);
920
974
  } else {
921
975
  input = element("input");
922
976
  input.type = "text";
923
- if (property.default !== undefined) input.value = property.default;
977
+ if (property.default !== undefined && property.default !== null) input.value = property.default;
924
978
  }
925
979
 
926
980
  input.dataset.name = name;
927
981
  input.dataset.kind = kind;
928
982
  if (isRequired) input.dataset.required = "1";
929
- row.append(label, input);
983
+ input.id = `argument-${++fieldId}`;
984
+ label.htmlFor = input.id;
985
+ if (kind === "choices") {
986
+ label.id = `${input.id}-label`;
987
+ input.setAttribute("aria-labelledby", label.id);
988
+ }
989
+ const heading = nullable ? element("div", "argument-heading") : row;
990
+ heading.append(label);
991
+ if (nullable) {
992
+ const toggleLabel = element("label", "null-toggle");
993
+ const toggle = element("input");
994
+ toggle.type = "checkbox";
995
+ toggle.checked = property.default === null || property.type === "null" || choices?.length === 0;
996
+ toggle.onchange = () => { input.disabled = toggle.checked; };
997
+ toggle.onchange();
998
+ input.nullToggle = toggle;
999
+ toggleLabel.append(toggle, element("span", null, "null"));
1000
+ heading.append(toggleLabel);
1001
+ row.append(heading);
1002
+ }
1003
+ row.append(input);
930
1004
  if (property.description) row.append(element("div", "hint", property.description));
931
1005
  into.append(row);
932
1006
  return input;
@@ -941,7 +1015,8 @@ function inferType(property) {
941
1015
  /* Empty inputs represent absent arguments, including for required-field validation. */
942
1016
  function missing(fields) {
943
1017
  return fields.filter(
944
- (input) => input.dataset.required && input.dataset.kind !== "boolean" && !input.value.trim()
1018
+ (input) => input.dataset.required && !input.nullToggle?.checked &&
1019
+ !["boolean", "choices"].includes(input.dataset.kind) && !input.value.trim()
945
1020
  );
946
1021
  }
947
1022
 
@@ -950,14 +1025,32 @@ function readFields(fields) {
950
1025
  fields.forEach((input) => {
951
1026
  const name = input.dataset.name;
952
1027
  const kind = input.dataset.kind;
1028
+ if (input.nullToggle?.checked) {
1029
+ values[name] = null;
1030
+ return;
1031
+ }
1032
+ if (kind === "choices") {
1033
+ values[name] = input.choiceInputs.filter((option) => option.checked)
1034
+ .map((option) => JSON.parse(option.value));
1035
+ return;
1036
+ }
953
1037
  if (kind === "boolean") {
954
1038
  values[name] = input.checked;
955
1039
  return;
956
1040
  }
957
1041
  const raw = input.value.trim();
1042
+ if (kind === "integer" || kind === "number") {
1043
+ if (input.validity.badInput) throw new Error(`${name}: enter a valid number.`);
1044
+ if (raw === "") return;
1045
+ const value = input.valueAsNumber;
1046
+ if (!Number.isFinite(value) || (kind === "integer" && !Number.isInteger(value))) {
1047
+ throw new Error(`${name}: enter a valid ${kind}.`);
1048
+ }
1049
+ values[name] = value;
1050
+ return;
1051
+ }
958
1052
  if (raw === "") return; // absent, which is not the same as empty
959
- if (kind === "integer") values[name] = parseInt(raw, 10);
960
- else if (kind === "number") values[name] = parseFloat(raw);
1053
+ if (kind === "choice") values[name] = JSON.parse(raw);
961
1054
  else if (kind === "object" || kind === "array") {
962
1055
  try {
963
1056
  values[name] = JSON.parse(raw);
@@ -970,8 +1063,56 @@ function readFields(fields) {
970
1063
  }
971
1064
 
972
1065
 
1066
+ function rememberToolFields(name, fields) {
1067
+ const key = `mcp-console-form:${JSON.stringify([location.pathname, endpointUrl.href, name])}`;
1068
+ let saved;
1069
+ try {
1070
+ saved = JSON.parse(localStorage.getItem(key));
1071
+ } catch {
1072
+ // Storage can be unavailable or contain an incomplete draft.
1073
+ }
1074
+ for (const input of fields) {
1075
+ const state = saved && Object.hasOwn(saved, input.dataset.name) ? saved[input.dataset.name] : null;
1076
+ if (!state || state.kind !== input.dataset.kind) continue;
1077
+ if (input.choiceInputs && Array.isArray(state.value)) {
1078
+ input.choiceInputs.forEach((option) => { option.checked = state.value.includes(option.value); });
1079
+ } else if (input.dataset.kind === "boolean" && typeof state.value === "boolean") {
1080
+ input.checked = state.value;
1081
+ } else if (typeof state.value === "string") {
1082
+ if (input.dataset.kind !== "choice" || [...input.options].some((option) => option.value === state.value)) {
1083
+ input.value = state.value;
1084
+ }
1085
+ }
1086
+ if (input.nullToggle && typeof state.isNull === "boolean") {
1087
+ input.nullToggle.checked = state.isNull;
1088
+ input.nullToggle.onchange();
1089
+ }
1090
+ }
1091
+ const save = () => {
1092
+ const entries = fields.map((input) => [input.dataset.name, {
1093
+ kind: input.dataset.kind,
1094
+ value: input.choiceInputs
1095
+ ? input.choiceInputs.filter((option) => option.checked).map((option) => option.value)
1096
+ : input.dataset.kind === "boolean" ? input.checked : input.value,
1097
+ isNull: input.nullToggle?.checked || false,
1098
+ }]);
1099
+ try {
1100
+ localStorage.setItem(key, JSON.stringify(Object.fromEntries(entries)));
1101
+ } catch {
1102
+ // Keep the form usable when browser storage is blocked or full.
1103
+ }
1104
+ };
1105
+ for (const input of fields) {
1106
+ input.addEventListener("input", save);
1107
+ input.addEventListener("change", save);
1108
+ if (input.nullToggle) input.nullToggle.addEventListener("change", save);
1109
+ }
1110
+ }
1111
+
1112
+
973
1113
  function show(items, title, into, describe, pick) {
974
- if (!items.length) return;
1114
+ if (!items.length) return [];
1115
+ const buttons = [];
975
1116
  const group = element("div", "group");
976
1117
  group.append(element("h3", null, title));
977
1118
  items.forEach((item) => {
@@ -990,8 +1131,10 @@ function show(items, title, into, describe, pick) {
990
1131
  pick(item);
991
1132
  };
992
1133
  group.append(button);
1134
+ buttons.push(button);
993
1135
  });
994
1136
  into.append(group);
1137
+ return buttons;
995
1138
  }
996
1139
 
997
1140
  function firstLine(text) {
@@ -1139,7 +1282,8 @@ function plain(text) {
1139
1282
 
1140
1283
 
1141
1284
  function show(items, title, into, describe, pick) {
1142
- if (!items.length) return;
1285
+ if (!items.length) return [];
1286
+ const buttons = [];
1143
1287
  const group = element("div", "group");
1144
1288
  group.append(element("h3", null, title));
1145
1289
  items.forEach((item) => {
@@ -1158,8 +1302,10 @@ function show(items, title, into, describe, pick) {
1158
1302
  pick(item);
1159
1303
  };
1160
1304
  group.append(button);
1305
+ buttons.push(button);
1161
1306
  });
1162
1307
  into.append(group);
1308
+ return buttons;
1163
1309
  }
1164
1310
 
1165
1311
  function hints(tool) {
@@ -1175,7 +1321,7 @@ async function loadCatalogue() {
1175
1321
  const into = document.createDocumentFragment();
1176
1322
 
1177
1323
  const tools = await client.listTools();
1178
- show(tools, `Tools (${tools.length})`, into, {
1324
+ const toolButtons = show(tools, `Tools (${tools.length})`, into, {
1179
1325
  label: (tool) => tool.name,
1180
1326
  note: (tool) => firstLine(tool.description),
1181
1327
  tags: hints,
@@ -1223,11 +1369,29 @@ async function loadCatalogue() {
1223
1369
  } else {
1224
1370
  page.catalogue.append(into);
1225
1371
  }
1372
+ restoreTool(tools, toolButtons);
1226
1373
  }
1227
1374
 
1375
+ function restoreTool(tools, buttons) {
1376
+ let name;
1377
+ try {
1378
+ name = decodeURIComponent(location.hash.slice(1));
1379
+ } catch {
1380
+ return;
1381
+ }
1382
+ const index = tools.findIndex((tool) => tool.name === name);
1383
+ if (index !== -1) buttons[index].click();
1384
+ }
1385
+
1386
+ function rememberTool(name) {
1387
+ const url = new URL(location.href);
1388
+ url.hash = name === null ? "" : encodeURIComponent(name);
1389
+ history.replaceState(history.state, "", url);
1390
+ }
1228
1391
 
1229
1392
  function choose(what) {
1230
1393
  chosen = what;
1394
+ rememberTool(what.kind === "tool" ? what.item.name : null);
1231
1395
  if (catalogue) catalogue.fold();
1232
1396
  page.outcome.textContent = "";
1233
1397
  page.args.textContent = "";
@@ -1238,6 +1402,7 @@ function choose(what) {
1238
1402
  prose(what.item.description, page.about);
1239
1403
  // Hide retry arguments managed by this client.
1240
1404
  chosen.fields = buildFields(what.item.inputSchema || {}, page.args, DRIVEN);
1405
+ rememberToolFields(what.item.name, chosen.fields);
1241
1406
  page.invoke.textContent = "Call";
1242
1407
  } else if (what.kind === "prompt") {
1243
1408
  page.subject.textContent = what.item.name;
@@ -1697,10 +1862,29 @@ async function invoke() {
1697
1862
  }
1698
1863
 
1699
1864
 
1865
+ function setServerInstructions(text) {
1866
+ serverInstructions = text || "";
1867
+ if (page.serverTitle) page.serverTitle.disabled = !serverInstructions;
1868
+ }
1869
+
1870
+ function showServerInstructions(clearFragment = true) {
1871
+ if (!serverInstructions) return;
1872
+ chosen = null;
1873
+ if (clearFragment) rememberTool(null);
1874
+ page.catalogue.querySelectorAll(".item").forEach((item) => item.classList.remove("chosen"));
1875
+ page.subject.textContent = "Instructions";
1876
+ prose(serverInstructions, page.about);
1877
+ page.args.textContent = "";
1878
+ page.outcome.textContent = "";
1879
+ page.invoke.disabled = true;
1880
+ if (catalogue) catalogue.fold();
1881
+ }
1882
+
1700
1883
  function disconnect() {
1701
1884
  // Sessions expire server-side; there is no close request.
1702
1885
  client?.abort.abort();
1703
1886
  client = null;
1887
+ setServerInstructions("");
1704
1888
  chosen = null;
1705
1889
  page.catalogue.textContent = "";
1706
1890
  page.catalogue.append(element("p", "empty", "Connect to see what this server offers."));
@@ -1737,17 +1921,15 @@ async function connect() {
1737
1921
  onQuestion: askPerson,
1738
1922
  });
1739
1923
  const described = await client.initialize();
1924
+ setServerInstructions(described.instructions);
1740
1925
  await loadCatalogue();
1741
1926
  page.refresh.disabled = false;
1742
1927
  const name = (described.serverInfo || (described._meta || {})["io.modelcontextprotocol/serverInfo"] || {}).name;
1743
1928
  say(`${name || "connected"} · ${client.version}`, "on");
1744
1929
  page.connect.textContent = "Disconnect";
1745
- if (described.instructions) {
1746
- page.subject.textContent = "Instructions";
1747
- prose(described.instructions, page.about);
1748
- page.invoke.disabled = true;
1749
- }
1930
+ if (!chosen) showServerInstructions(false);
1750
1931
  } catch (error) {
1932
+ setServerInstructions("");
1751
1933
  client = null;
1752
1934
  page.refresh.disabled = true;
1753
1935
  page.revision.disabled = false;
@@ -1825,6 +2007,7 @@ function markTraffic() {
1825
2007
 
1826
2008
 
1827
2009
  function start() {
2010
+ if (page.serverTitle) page.serverTitle.onclick = () => showServerInstructions();
1828
2011
  setupAuthentication();
1829
2012
  Object.keys(REVISIONS).forEach((version) => {
1830
2013
  const option = element("option", null, version);
@@ -1846,7 +2029,10 @@ function start() {
1846
2029
  page.connect.onclick = () => (client ? disconnect() : connect());
1847
2030
  page.refresh.onclick = async () => {
1848
2031
  try {
1849
- if (client.rules.extensions) await client.initialize();
2032
+ if (client.rules.extensions) {
2033
+ const described = await client.initialize();
2034
+ setServerInstructions(described.instructions);
2035
+ }
1850
2036
  await loadCatalogue();
1851
2037
  } catch (error) {
1852
2038
  say(error.message, "off");
@@ -10,7 +10,7 @@
10
10
  <body>
11
11
 
12
12
  <header class="bar">
13
- <h1>{{TITLE}}</h1>
13
+ <h1><button id="server-title" type="button" title="Show server instructions" disabled>{{TITLE}}</button></h1>
14
14
  <label class="field grow" title="The path the server serves MCP on. A query string is sent as typed">
15
15
  <span>Endpoint</span>
16
16
  <input id="endpoint" class="endpoint" type="text" spellcheck="false" autocomplete="off">