diff --git a/caosdb-proto b/caosdb-proto
index c6405e538c179d2a8af952f85d9e9dc51fbadb92..79549b60e441c0b2d693ce189d6e189fed331a7c 160000
--- a/caosdb-proto
+++ b/caosdb-proto
@@ -1 +1 @@
-Subproject commit c6405e538c179d2a8af952f85d9e9dc51fbadb92
+Subproject commit 79549b60e441c0b2d693ce189d6e189fed331a7c
diff --git a/src/main/java/org/caosdb/server/grpc/GRPCServer.java b/src/main/java/org/caosdb/server/grpc/GRPCServer.java
index 494ce94706eff75415264b175f58cdaf16a2a442..ee2bb036d0911136834d230b8994ef841aca8e19 100644
--- a/src/main/java/org/caosdb/server/grpc/GRPCServer.java
+++ b/src/main/java/org/caosdb/server/grpc/GRPCServer.java
@@ -141,6 +141,12 @@ public class GRPCServer {
         ServerInterceptors.intercept(
             entityTransactionService, loggingInterceptor, authInterceptor));
 
+    final ServerSideScriptingServiceImpl serverSideScriptingService =
+        new ServerSideScriptingServiceImpl();
+    services.add(
+        ServerInterceptors.intercept(
+            serverSideScriptingService, loggingInterceptor, authInterceptor));
+
     return services;
   }
 
diff --git a/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java b/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..99ed6a2970dab9369927b169fa7fe8018a34080f
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/ServerSideScriptingServiceImpl.java
@@ -0,0 +1,284 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2025 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2025 Joscha Schmiedt <joscha@schmiedt.dev>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.caosdb.server.grpc;
+
+import com.google.protobuf.Timestamp;
+import io.grpc.Status;
+import io.grpc.StatusException;
+import io.grpc.stub.StreamObserver;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.UnauthorizedException;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptRequest;
+import org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptResponse;
+import org.caosdb.api.scripting.v1alpha1.NamedArgument;
+import org.caosdb.api.scripting.v1alpha1.ServerSideScriptExecutionId;
+import org.caosdb.api.scripting.v1alpha1.ServerSideScriptExecutionResult;
+import org.caosdb.api.scripting.v1alpha1.ServerSideScriptingServiceGrpc.ServerSideScriptingServiceImplBase;
+import org.caosdb.server.accessControl.AuthenticationUtils;
+import org.caosdb.server.accessControl.OneTimeAuthenticationToken;
+import org.caosdb.server.accessControl.SessionToken;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.scripting.ScriptingPermissions;
+import org.caosdb.server.scripting.ServerSideScriptingCaller;
+import org.caosdb.server.utils.ServerMessages;
+
+/**
+ * Main entry point for the server-side scripting service of the server's GRPC API.
+ *
+ * @author Joscha Schmiedt <joscha@schmiedt.dev>
+ */
+public class ServerSideScriptingServiceImpl extends ServerSideScriptingServiceImplBase {
+
+  private ServerSideScriptingCaller caller;
+  private static AtomicLong executionId = new AtomicLong(0);
+
+  @Override
+  public void executeServerSideScript(
+      ExecuteServerSideScriptRequest request,
+      io.grpc.stub.StreamObserver<org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptResponse>
+          responseObserver) {
+
+    try {
+      AuthInterceptor.bindSubject();
+      ExecuteServerSideScriptResponse response = callScript(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      ServerSideScriptingServiceImpl.handleException(responseObserver, e);
+    }
+  }
+
+  private long incrementAndGetExecutionId() {
+    return executionId.incrementAndGet();
+  }
+
+  /**
+   * Converts the ExecuteServerSideScriptRequest to a command line argument list.
+   *
+   * @param request the request containing the script filename and arguments
+   * @return the command line argument list
+   * @throws IllegalArgumentException if the script filename is missing
+   */
+  public static ArrayList<String> request2CommandLine(ExecuteServerSideScriptRequest request)
+      throws IllegalArgumentException {
+
+    if (request.getScriptFilename().length() == 0) {
+      throw new IllegalArgumentException("Script filename is missing in the request.");
+    }
+
+    ArrayList<String> commandLine =
+        new ArrayList<>(
+            1 + request.getPositionalArgumentsCount() + request.getNamedArgumentsCount());
+
+    // first item is the script filename
+    commandLine.add(request.getScriptFilename());
+
+    // add named arguments
+    for (NamedArgument namedArgument : request.getNamedArgumentsList()) {
+      String name = namedArgument.getName();
+      String value = namedArgument.getValue();
+      commandLine.add(String.format("--%s=%s", name, value));
+    }
+
+    // add positional arguments
+    for (String positionalArgument : request.getPositionalArgumentsList()) {
+      commandLine.add(positionalArgument);
+    }
+
+    return commandLine;
+  }
+
+  /////////////////////////////////////////////////////////////////////////////////////////
+  // TODO: isAnonymous, getUser and generateAuthToken are copy-pasted from
+  // elsewhere. Consider refactoring.
+  /////////////////////////////////////////////////////////////////////////////////////////
+  private static boolean isAnonymous() {
+    return AuthenticationUtils.isAnonymous(getUser());
+  }
+
+  private static Subject getUser() {
+    Subject ret = SecurityUtils.getSubject();
+    return ret;
+  }
+
+  private static String[] constructCallStringArray(ArrayList<String> commandLine) {
+    return commandLine.toArray(new String[commandLine.size()]);
+  }
+
+  private static Object generateAuthToken(String call) {
+    String purpose = "SCRIPTING:EXECUTE:" + call;
+    Object authtoken = OneTimeAuthenticationToken.generateForPurpose(purpose, getUser());
+    if (authtoken != null || isAnonymous()) {
+      return authtoken;
+    }
+    return SessionToken.generate(getUser());
+  }
+
+  private static void checkExecutionPermission(Subject user, String call)
+      throws AuthorizationException {
+    user.checkPermission(ScriptingPermissions.PERMISSION_EXECUTION(call));
+  }
+
+  /////////////////////////////////////////////////////////////////////////////////////////
+
+  public ExecuteServerSideScriptResponse callScript(ExecuteServerSideScriptRequest request)
+      throws Message {
+
+    long startTimeMillis = System.currentTimeMillis();
+    Timestamp startTime = Timestamp.newBuilder().setSeconds(startTimeMillis / 1000).build();
+
+    checkExecutionPermission(getUser(), request.getScriptFilename());
+
+    String[] call = constructCallStringArray(request2CommandLine(request));
+
+    Integer timeoutMs = request.getTimeoutMs() != 0 ? (int) request.getTimeoutMs() : -1;
+
+    if (request.getScriptFilesCount() > 0) {
+      throw new UnsupportedOperationException(
+          "Uploading files for scripting via gRPC is not supported yet.");
+    }
+
+    // auth token
+    Object authToken = request.getAuthToken();
+    if (authToken == null) {
+      authToken = generateAuthToken(request.getScriptFilename());
+    }
+
+    // invoke script
+    caller =
+        new ServerSideScriptingCaller(call, timeoutMs, new ArrayList<FileProperties>(), authToken);
+    int returnCode = caller.invoke();
+
+    // construct response
+    String scriptExecutionId = String.valueOf(incrementAndGetExecutionId());
+    String scriptFilename = request.getScriptFilename();
+    ServerSideScriptExecutionResult result =
+        ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_SUCCESS;
+
+    String stdout = "";
+    try {
+      stdout = caller.getStdOut();
+    } catch (final IOException e) {
+      stdout = "Error retreiving stdout";
+      result = ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_GENERAL_FAILURE;
+    }
+
+    String stderr = "";
+    try {
+      stderr = caller.getStdErr();
+    } catch (final IOException e) {
+      stderr = "Error retrieving stderr";
+      result = ServerSideScriptExecutionResult.SERVER_SIDE_SCRIPT_EXECUTION_RESULT_GENERAL_FAILURE;
+    }
+
+    long endTimeMillis = System.currentTimeMillis();
+    Timestamp endTime = Timestamp.newBuilder().setSeconds(endTimeMillis / 1000).build();
+    int duration_ms = (int) (endTimeMillis - startTimeMillis);
+
+    final ExecuteServerSideScriptResponse response =
+        ExecuteServerSideScriptResponse.newBuilder()
+            .setScriptExecutionId(
+                ServerSideScriptExecutionId.newBuilder()
+                    .setScriptExecutionId(scriptExecutionId)
+                    .build())
+            .setCall(scriptFilename)
+            .setResult(result)
+            .setReturnCode(returnCode)
+            .setStdout(stdout)
+            .setStderr(stderr)
+            .setStartTime(startTime)
+            .setEndTime(endTime)
+            .setDurationMs(duration_ms)
+            .build();
+    return response;
+  }
+
+  public static void handleException(StreamObserver<?> responseObserver, Exception e) {
+    String description = e.getMessage();
+    if (description == null || description.isBlank()) {
+      description = "Unknown Error. Please Report!";
+    }
+    if (e instanceof UnauthorizedException) {
+      Subject subject = SecurityUtils.getSubject();
+      if (AuthenticationUtils.isAnonymous(subject)) {
+        responseObserver.onError(new StatusException(AuthInterceptor.PLEASE_LOG_IN));
+        return;
+      } else {
+        responseObserver.onError(
+            new StatusException(
+                Status.PERMISSION_DENIED.withCause(e).withDescription(description)));
+        return;
+      }
+    } else if (e == ServerMessages.ROLE_DOES_NOT_EXIST
+        || e == ServerMessages.ACCOUNT_DOES_NOT_EXIST) {
+      responseObserver.onError(
+          new StatusException(Status.NOT_FOUND.withDescription(description).withCause(e)));
+      return;
+    }
+    else if (e == ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST)
+    {
+      responseObserver.onError(
+          new StatusException(Status.NOT_FOUND.withDescription(description).withCause(e)));
+      return;
+    }
+    else if (e == ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE)
+    {
+      responseObserver.onError(
+          new StatusException(Status.PERMISSION_DENIED.withDescription(description).withCause(e)));
+      return;
+    }
+    else if (e == ServerMessages.SERVER_SIDE_SCRIPT_ERROR)
+    {
+      responseObserver.onError(
+          new StatusException(Status.UNKNOWN.withDescription(description).withCause(e)));
+      return;
+    }
+    else if (e == ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR)
+    {
+      responseObserver.onError(
+          new StatusException(Status.UNKNOWN.withDescription(description).withCause(e)));
+      return;
+    }
+    else if (e == ServerMessages.SERVER_SIDE_SCRIPT_TIMEOUT)
+    {
+      responseObserver.onError(
+          new StatusException(Status.DEADLINE_EXCEEDED.withDescription(description).withCause(e)));
+      return;
+    }
+    else if (e == ServerMessages.SERVER_SIDE_SCRIPT_MISSING_CALL)
+    {
+      responseObserver.onError(
+          new StatusException(Status.INVALID_ARGUMENT.withDescription(description).withCause(e)));
+      return;
+    }
+    
+    e.printStackTrace();
+    responseObserver.onError(
+        new StatusException(Status.UNKNOWN.withDescription(description).withCause(e)));
+  }
+}
diff --git a/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java b/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..013b239771ee8d85a9854398a99467c6509f6738
--- /dev/null
+++ b/src/test/java/org/caosdb/server/grpc/ServerSideScriptingGrpcTest.java
@@ -0,0 +1,55 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2025 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2025 Joscha Schmiedt <joscha@schmiedt.dev>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.caosdb.server.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import org.caosdb.api.scripting.v1alpha1.ExecuteServerSideScriptRequest;
+import org.caosdb.api.scripting.v1alpha1.NamedArgument;
+import org.caosdb.server.entity.Message;
+import org.junit.jupiter.api.Test;
+
+public class ServerSideScriptingGrpcTest {
+
+  @Test
+  public void testExecuteServerSideScriptRequestToCommandline() throws Message {
+    ExecuteServerSideScriptRequest request =
+        ExecuteServerSideScriptRequest.newBuilder()
+            .setScriptFilename("my_script.py")
+            .addPositionalArguments("pos0")
+            .addPositionalArguments("pos1")
+            .addNamedArguments(
+                NamedArgument.newBuilder().setName("option1").setValue("val1").build())
+            .addNamedArguments(
+                NamedArgument.newBuilder().setName("option2").setValue("val2").build())
+            // .putFiles("-Ofile", "test_file.txt")
+            .build();
+
+    ArrayList<String> commandline = ServerSideScriptingServiceImpl.request2CommandLine(request);
+
+    assertEquals(commandline.get(0), "my_script.py");
+    assertEquals(commandline.get(1), "--option1=val1");
+    assertEquals(commandline.get(2), "--option2=val2");
+    assertEquals(commandline.get(3), "pos0");
+    assertEquals(commandline.get(4), "pos1");
+  }
+}