@humanjs/mcp 0.2.0 → 0.4.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/README.md +2 -0
- package/dist/index.cjs +149 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +149 -1
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ function readEnv() {
|
|
|
20
20
|
speed: parseSpeed(process.env.HUMANJS_SPEED),
|
|
21
21
|
headless: parseBool(process.env.HUMANJS_HEADLESS, false),
|
|
22
22
|
outputDir: process.env.HUMANJS_OUTPUT_DIR ?? process.cwd(),
|
|
23
|
+
uploadDir: process.env.HUMANJS_UPLOAD_DIR ?? process.cwd(),
|
|
23
24
|
viewport: parseViewport(process.env.HUMANJS_VIEWPORT),
|
|
24
25
|
autoInstall: parseBool(process.env.HUMANJS_AUTO_INSTALL, true),
|
|
25
26
|
browser: resolveBrowserConfig(),
|
|
@@ -589,6 +590,15 @@ function resolveOutputPath(outputDir, filename) {
|
|
|
589
590
|
}
|
|
590
591
|
return join(outputDir, base);
|
|
591
592
|
}
|
|
593
|
+
function resolveUploadPath(uploadDir, filename) {
|
|
594
|
+
const base = basename(filename);
|
|
595
|
+
if (base !== filename || base.length === 0) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
`upload filename must be a plain name with no path components, got "${filename}". Files are read from HUMANJS_UPLOAD_DIR \u2014 place the file there (or point HUMANJS_UPLOAD_DIR at its folder) and pass just the name.`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return join(uploadDir, base);
|
|
601
|
+
}
|
|
592
602
|
function resolveRecordingFormat(filename) {
|
|
593
603
|
const lower = filename.toLowerCase();
|
|
594
604
|
if (lower.endsWith(".mp4") || lower.endsWith(".webm")) return "video";
|
|
@@ -643,6 +653,22 @@ function registerInspectionTools(server, ctx) {
|
|
|
643
653
|
return { content: [{ type: "text", text }] };
|
|
644
654
|
}
|
|
645
655
|
);
|
|
656
|
+
server.registerTool(
|
|
657
|
+
"human_outline",
|
|
658
|
+
{
|
|
659
|
+
title: "Page outline (accessibility tree)",
|
|
660
|
+
description: 'Returns a compact accessibility-tree outline of the page (or a region) \u2014 every interactive element and landmark by its ARIA role + accessible name, as YAML (e.g. `- button "Sign in"`, `- textbox "Email"`). The most token-efficient way to see what is actionable and pick a selector: the names map directly to getByRole / accessible-name selectors. Prefer this over human_get_html for "what can I click or fill"; use human_screenshot when you need the visual layout.',
|
|
661
|
+
inputSchema: {
|
|
662
|
+
selector: z.string().optional().describe("Optional region selector to scope the outline. Omit for the whole page."),
|
|
663
|
+
session: sessionArg
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
async ({ selector, session }) => {
|
|
667
|
+
const { human } = await ctx.sessions.get(session);
|
|
668
|
+
const text = await human.outline(selector);
|
|
669
|
+
return { content: [{ type: "text", text }] };
|
|
670
|
+
}
|
|
671
|
+
);
|
|
646
672
|
server.registerTool(
|
|
647
673
|
"human_get_text",
|
|
648
674
|
{
|
|
@@ -721,7 +747,7 @@ function resolveTarget(input) {
|
|
|
721
747
|
var sessionArg2 = z.string().optional().describe(
|
|
722
748
|
"Session ID to act on. Omit to use the default session (created lazily on first call). Use human_create_session for parallel browsers."
|
|
723
749
|
);
|
|
724
|
-
function registerPrimitiveTools(server, { sessions }) {
|
|
750
|
+
function registerPrimitiveTools(server, { sessions, env }) {
|
|
725
751
|
server.registerTool(
|
|
726
752
|
"human_goto",
|
|
727
753
|
{
|
|
@@ -768,6 +794,22 @@ function registerPrimitiveTools(server, { sessions }) {
|
|
|
768
794
|
};
|
|
769
795
|
}
|
|
770
796
|
);
|
|
797
|
+
server.registerTool(
|
|
798
|
+
"human_doubleClick",
|
|
799
|
+
{
|
|
800
|
+
title: "Double-click (humanized)",
|
|
801
|
+
description: "Double-clicks the target \u2014 same humanized motion as human_click, but two presses within the OS double-click window. Use for things that open/activate on double-click (list rows, file items, editable cells). Target is a selector OR x/y coordinates.",
|
|
802
|
+
inputSchema: { ...targetFields, session: sessionArg2 }
|
|
803
|
+
},
|
|
804
|
+
async ({ selector, x, y, session }) => {
|
|
805
|
+
const { human } = await sessions.get(session);
|
|
806
|
+
const target = resolveTarget({ selector, x, y });
|
|
807
|
+
await human.doubleClick(target);
|
|
808
|
+
return {
|
|
809
|
+
content: [{ type: "text", text: `double-clicked ${describeTarget(selector, x, y)}` }]
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
);
|
|
771
813
|
server.registerTool(
|
|
772
814
|
"human_hover",
|
|
773
815
|
{
|
|
@@ -862,6 +904,112 @@ function registerPrimitiveTools(server, { sessions }) {
|
|
|
862
904
|
return { content: [{ type: "text", text: `pasted ${value.length} chars into ${selector}` }] };
|
|
863
905
|
}
|
|
864
906
|
);
|
|
907
|
+
server.registerTool(
|
|
908
|
+
"human_clear",
|
|
909
|
+
{
|
|
910
|
+
title: "Clear a field (humanized)",
|
|
911
|
+
description: "Clears a text field (input/textarea/contenteditable) with a real keyboard gesture \u2014 click to focus, select-all, then delete \u2014 firing the input events the page expects. Use before human_type when you need to replace an existing value rather than append to it.",
|
|
912
|
+
inputSchema: {
|
|
913
|
+
selector: z.string().describe("Selector of the field to clear."),
|
|
914
|
+
session: sessionArg2
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
async ({ selector, session }) => {
|
|
918
|
+
const { human } = await sessions.get(session);
|
|
919
|
+
await human.clear(selector);
|
|
920
|
+
return { content: [{ type: "text", text: `cleared ${selector}` }] };
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
server.registerTool(
|
|
924
|
+
"human_selectText",
|
|
925
|
+
{
|
|
926
|
+
title: "Select an element\u2019s text (humanized)",
|
|
927
|
+
description: "Selects (highlights) text inside an element \u2014 moves the cursor to it, then selects. By default selects all of the element\u2019s text; pass `text` to select just that substring (found inside the element, whitespace-tolerant, first match; falls back to the whole element if not found). Use before copying, replacing, or triggering a highlight menu.",
|
|
928
|
+
inputSchema: {
|
|
929
|
+
selector: z.string().describe("Selector of the element whose text to select."),
|
|
930
|
+
text: z.string().optional().describe("Optional substring to select instead of the element\u2019s whole text."),
|
|
931
|
+
session: sessionArg2
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
async ({ selector, text, session }) => {
|
|
935
|
+
const { human } = await sessions.get(session);
|
|
936
|
+
await human.selectText(selector, text === void 0 ? void 0 : { text });
|
|
937
|
+
const what = text === void 0 ? "text" : `"${text}"`;
|
|
938
|
+
return { content: [{ type: "text", text: `selected ${what} in ${selector}` }] };
|
|
939
|
+
}
|
|
940
|
+
);
|
|
941
|
+
server.registerTool(
|
|
942
|
+
"human_check",
|
|
943
|
+
{
|
|
944
|
+
title: "Check a box (humanized)",
|
|
945
|
+
description: "Ticks a checkbox or radio \u2014 moves the cursor to it and clicks, but only if it is not already checked (a real user does not re-click a ticked box). Verifies the resulting state. Pass the checkbox/radio input itself (or a [role=checkbox]) \u2014 not a wrapping <label> \u2014 so the current state can be read and the click stays idempotent.",
|
|
946
|
+
inputSchema: {
|
|
947
|
+
selector: z.string().describe("Selector of the checkbox/radio input."),
|
|
948
|
+
session: sessionArg2
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
async ({ selector, session }) => {
|
|
952
|
+
const { human } = await sessions.get(session);
|
|
953
|
+
await human.check(selector);
|
|
954
|
+
return { content: [{ type: "text", text: `checked ${selector}` }] };
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
server.registerTool(
|
|
958
|
+
"human_uncheck",
|
|
959
|
+
{
|
|
960
|
+
title: "Uncheck a box (humanized)",
|
|
961
|
+
description: "Unticks a checkbox \u2014 humanized click only if currently checked. Radios cannot be unchecked by clicking (select a different option instead). Pass the checkbox input itself (or a [role=checkbox]) \u2014 not a wrapping <label> \u2014 so its state can be read and the click stays idempotent.",
|
|
962
|
+
inputSchema: {
|
|
963
|
+
selector: z.string().describe("Selector of the checkbox input."),
|
|
964
|
+
session: sessionArg2
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
async ({ selector, session }) => {
|
|
968
|
+
const { human } = await sessions.get(session);
|
|
969
|
+
await human.uncheck(selector);
|
|
970
|
+
return { content: [{ type: "text", text: `unchecked ${selector}` }] };
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
server.registerTool(
|
|
974
|
+
"human_selectOption",
|
|
975
|
+
{
|
|
976
|
+
title: "Select dropdown option (humanized)",
|
|
977
|
+
description: "Chooses option(s) in a native <select> \u2014 moves the cursor to the dropdown, then sets the value (native selects open an OS menu automation can't drive, so the value is set programmatically, firing change/input). For custom DOM dropdowns, use human_click on the rendered options instead. Match by value(s); pass one string or an array for multi-selects.",
|
|
978
|
+
inputSchema: {
|
|
979
|
+
selector: z.string().describe("Selector of the <select> element."),
|
|
980
|
+
values: z.union([z.string(), z.array(z.string())]).describe("Option value, or array of values for a multi-select."),
|
|
981
|
+
session: sessionArg2
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
async ({ selector, values, session }) => {
|
|
985
|
+
const { human } = await sessions.get(session);
|
|
986
|
+
const selected = await human.selectOption(selector, values);
|
|
987
|
+
return {
|
|
988
|
+
content: [{ type: "text", text: `selected ${selected.join(", ")} in ${selector}` }]
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
server.registerTool(
|
|
993
|
+
"human_upload",
|
|
994
|
+
{
|
|
995
|
+
title: "Upload file(s) (humanized)",
|
|
996
|
+
description: `Attaches file(s) to a file input \u2014 moves the cursor to the control, then sets the files (never opens the OS dialog, which would hang). For safety, files are read by basename from HUMANJS_UPLOAD_DIR (default: the server working dir) \u2014 subdirectories, "../", and absolute paths are rejected, so the agent can't read and exfiltrate arbitrary local files. Pass the <input type="file"> selector and the filename(s).`,
|
|
997
|
+
inputSchema: {
|
|
998
|
+
selector: z.string().describe("Selector of the file input."),
|
|
999
|
+
files: z.union([z.string(), z.array(z.string())]).describe("Filename(s) inside HUMANJS_UPLOAD_DIR \u2014 a basename only, no path components."),
|
|
1000
|
+
session: sessionArg2
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
async ({ selector, files, session }) => {
|
|
1004
|
+
const { human } = await sessions.get(session);
|
|
1005
|
+
const names = Array.isArray(files) ? files : [files];
|
|
1006
|
+
const paths = names.map((name) => resolveUploadPath(env.uploadDir, name));
|
|
1007
|
+
await human.upload(selector, paths);
|
|
1008
|
+
return {
|
|
1009
|
+
content: [{ type: "text", text: `uploaded ${paths.length} file(s) to ${selector}` }]
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
865
1013
|
server.registerTool(
|
|
866
1014
|
"human_press",
|
|
867
1015
|
{
|