diff --git a/tools/usb/.gitignore b/tools/usb/.gitignore
index fce1ef5a9267..4645c8a1ca15 100644
--- a/tools/usb/.gitignore
+++ b/tools/usb/.gitignore
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 ffs-test
 testusb
+ucsi
diff --git a/tools/usb/Build b/tools/usb/Build
index 2ad6f9745816..f77e2ed05a63 100644
--- a/tools/usb/Build
+++ b/tools/usb/Build
@@ -1,2 +1,3 @@
 testusb-y += testusb.o
 ffs-test-y += ffs-test.o
+ucsi-y += ucsi.o
diff --git a/tools/usb/Makefile b/tools/usb/Makefile
index c6235667dd46..1670892a6d5a 100644
--- a/tools/usb/Makefile
+++ b/tools/usb/Makefile
@@ -16,7 +16,7 @@ MAKEFLAGS += -r
 override CFLAGS += -O2 -Wall -Wextra -g -D_GNU_SOURCE -I$(OUTPUT)include -I$(srctree)/tools/include
 override LDFLAGS += -lpthread
 
-ALL_TARGETS := testusb ffs-test
+ALL_TARGETS := testusb ffs-test ucsi
 ALL_PROGRAMS := $(patsubst %,$(OUTPUT)%,$(ALL_TARGETS))
 
 all: $(ALL_PROGRAMS)
@@ -36,6 +36,12 @@ $(FFS_TEST_IN): FORCE
 $(OUTPUT)ffs-test: $(FFS_TEST_IN)
 	$(QUIET_LINK)$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
 
+UCSI_IN := $(OUTPUT)ucsi-in.o
+$(UCSI_IN): FORCE
+	$(Q)$(MAKE) $(build)=ucsi
+$(OUTPUT)ucsi: $(UCSI_IN)
+	$(QUIET_LINK)$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+
 clean:
 	rm -f $(ALL_PROGRAMS)
 	find $(or $(OUTPUT),.) -name '*.o' -delete -o -name '\.*.d' -delete -o -name '\.*.o.cmd' -delete
diff --git a/tools/usb/ucsi.c b/tools/usb/ucsi.c
new file mode 100644
index 000000000000..4649f3a80d62
--- /dev/null
+++ b/tools/usb/ucsi.c
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * UCSI command testing tool
+ *
+ * Copyright (C) 2025, Intel Corporation
+ */
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/types.h>
+
+/* UCSI data structure field offsets */
+#define UCSI_VERSION			0
+#define UCSI_CCI			4
+#define UCSI_CONTROL			8
+#define UCSI_MESSAGE_IN			16
+
+#define CCI_DATA_LENGTH(cci)		(((cci) >> 8) & 0xff)
+
+static const char *class = "/sys/class/typec";
+static char path[PATH_MAX];
+
+#define MAX_INTERFACES			10
+
+static const char *find_dev(const char *dev, char **arr, size_t arr_size)
+{
+	size_t i;
+
+	for (i = 0; i < arr_size; i++)
+		if (!strcmp(dev, arr[i]))
+			return arr[i];
+	return NULL;
+}
+
+static int ucsi_match(const struct dirent *entry)
+{
+	snprintf(path, sizeof(path), "%s/%s/device/ucsi", class, entry->d_name);
+
+	return !access(path, F_OK);
+}
+
+static int find_devices(char **devs)
+{
+	struct dirent **dirs;
+	int ndevs = 0;
+	char *rpath;
+	int n;
+
+	n = scandir(class, &dirs, ucsi_match, alphasort);
+	if (n <= 0)
+		return 0;
+
+	while (n--) {
+		snprintf(path, sizeof(path), "%s/%s/device",
+			 class, dirs[n]->d_name);
+
+		rpath = realpath(path, NULL);
+
+		if (find_dev(rpath, devs, ndevs)) {
+			free(rpath);
+			continue;
+		}
+
+		devs[ndevs++] = rpath;
+
+		if (ndevs == MAX_INTERFACES) {
+			fprintf(stderr, "maximum number of interfaces reached\n");
+			break;
+		}
+	}
+
+	free(dirs);
+
+	return ndevs;
+}
+
+static int run_command(const char *mb, __u64 command)
+{
+	__u8 data[256] = { };
+	__u32 cci;
+	int ret;
+	int fd;
+	__u8 i;
+
+	snprintf(path, sizeof(path), "%s/ucsi", mb);
+
+	fd = open(path, O_RDWR);
+	if (fd < 0) {
+		fprintf(stderr, "failed to open \'%s\'\n", path);
+		return 1;
+	}
+
+	ret = pwrite(fd, &command, sizeof(command), UCSI_CONTROL);
+	if (ret < 0) {
+		fprintf(stderr, "Failed to send command (%d)\n", ret);
+		goto err;
+	}
+
+	ret = pread(fd, &cci, sizeof(cci), UCSI_CCI);
+	if (ret <= 0) {
+		fprintf(stderr, "failed to read CCI\n");
+		goto err;
+	}
+
+	if (CCI_DATA_LENGTH(cci)) {
+		ret = pread(fd, data, CCI_DATA_LENGTH(cci), UCSI_MESSAGE_IN);
+		if (ret <= 0) {
+			fprintf(stderr, "failed to read MESSAGE_IN (%d)\n", ret);
+			goto err;
+		}
+	}
+
+	/* Print everything. */
+
+	printf("\nCONTROL:\t0x%016llx\n", command);
+	printf("CCI:\t\t0x%08x\n", cci);
+
+	if (CCI_DATA_LENGTH(cci)) {
+		printf("MESSAGE_IN");
+		for (i = 0; i < CCI_DATA_LENGTH(cci); i++) {
+			if (!(i % 8)) {
+				if (i % 16)
+					printf(" ");
+				else
+					printf("\n%08d\t", i ? 10 * 16 / i : 0);
+			}
+			printf("%02x ", data[i]);
+		}
+		printf("\n");
+	}
+err:
+	close(fd);
+	return ret < 0 ? ret : 0;
+}
+
+static int print_version(const char *mb)
+{
+	__u16 version = 0;
+	int ret;
+	int fd;
+
+	snprintf(path, sizeof(path), "%s/ucsi", mb);
+
+	fd = open(path, O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "failed to open \'%s\'\n", path);
+		return fd;
+	}
+	ret = pread(fd, &version, sizeof(version), UCSI_VERSION);
+	close(fd);
+
+	if (ret <= 0) {
+		fprintf(stderr, "\'%s\' -- failed to read version (%d)\n", mb, ret);
+		return ret;
+	}
+
+	printf("%s - UCSI v%u.%u\n", mb,
+	       version >> 8 & 0xff, /* Major */
+	       version >> 4 & 0xf); /* Minor */
+	return 0;
+}
+
+static void usage(const char *name)
+{
+	fprintf(stderr,
+		"Usage: %s -l | [-d INTERFACE] COMMAND\n"
+		"Execute UCSI commands\n\n"
+		"  -l\tlist interfaces (devices)\n"
+		"  -d\tselect interface\n"
+		"  -h\tdisplay this help and exit\n"
+		"\nIf no interface is supplied, the first found is used.\n",
+		name);
+	fprintf(stderr, "Examples:\n"
+		"  %s 0x6      (GET_CAPABILITY)\n"
+		"  %s 0x10007  (GET_CONNECTOR_CAPABILITY)\n",
+		name, name);
+}
+
+int main(int argc, char *argv[])
+{
+	char *devs[MAX_INTERFACES];
+	const char *mb = NULL;
+	__u64 command = 0;
+	int ret = -EINVAL;
+	int n = 0;
+	char *end;
+	int opt;
+	int i;
+
+	n = find_devices(devs);
+
+	while ((opt = getopt(argc, argv, "d:hl")) != -1) {
+		switch (opt) {
+		case 'h':
+			usage(argv[0]);
+			goto out_free;
+		case 'l':
+			for (i = 0; i < n; i++) {
+				print_version(devs[i]);
+				free(devs[i]);
+			}
+			return 0;
+		case 'd':
+			mb = optarg;
+			if (!find_dev(mb, devs, n)) {
+				fprintf(stderr, "\'%s\' is not UCSI\n", mb);
+				goto out_free;
+			}
+			break;
+		default:
+			usage(argv[0]);
+			goto out_free;
+		}
+	}
+
+	if (!n) {
+		fprintf(stderr, "No UCSI found\n");
+		return 1;
+	}
+
+	if (optind >= argc) {
+		usage(argv[0]);
+		goto out_free;
+	}
+
+	command = strtoll(argv[optind], &end, 16);
+	if (errno == ERANGE || end == argv[optind] || *end != '\0') {
+		fprintf(stderr, "invalid command -- \'%s\'\n", argv[optind]);
+		goto out_free;
+	}
+
+	if (!mb)
+		mb = devs[0];
+
+	ret = print_version(mb);
+	if (ret)
+		goto out_free;
+
+	ret = run_command(mb, command);
+out_free:
+	for (i = 0; i < n; i++)
+		free(devs[i]);
+
+	return !!ret;
+}
